/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "SwipeTracker.h" #include "InputData.h" #include "mozilla/FlushType.h" #include "mozilla/PresShell.h" #include "mozilla/StaticPrefs_widget.h" #include "mozilla/StaticPrefs_browser.h" #include "mozilla/TimeStamp.h" #include "mozilla/TouchEvents.h" #include "mozilla/dom/SimpleGestureEventBinding.h" #include "nsAlgorithm.h" #include "nsIWidget.h" #include "nsRefreshDriver.h" #include "UnitTransforms.h" // These values were tweaked to make the physics feel similar to the native // swipe. static const double kSpringForce = 250.0; static const double kSwipeSuccessThreshold = 0.25; namespace mozilla { static already_AddRefed GetRefreshDriver(nsIWidget& aWidget) { nsIWidgetListener* widgetListener = aWidget.GetWidgetListener(); PresShell* presShell = widgetListener ? widgetListener->GetPresShell() : nullptr; nsPresContext* presContext = presShell ? presShell->GetPresContext() : nullptr; RefPtr refreshDriver = presContext ? presContext->RefreshDriver() : nullptr; return refreshDriver.forget(); } SwipeTracker::SwipeTracker(nsIWidget& aWidget, const PanGestureInput& aSwipeStartEvent, uint32_t aAllowedDirections, uint32_t aSwipeDirection) : mWidget(aWidget), mRefreshDriver(GetRefreshDriver(mWidget)), mAxis(0.0, 0.0, 0.0, kSpringForce, 1.0), mEventPosition(RoundedToInt(ViewAs( aSwipeStartEvent.mPanStartPoint, PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent))), mLastEventTimeStamp(aSwipeStartEvent.mTimeStamp), mAllowedDirections(aAllowedDirections), mSwipeDirection(aSwipeDirection), mGestureAmount(0.0), mCurrentVelocity(0.0), mEventsAreControllingSwipe(true), mEventsHaveStartedNewGesture(false), mRegisteredWithRefreshDriver(false) { SendSwipeEvent(eSwipeGestureStart, 0, 0.0, aSwipeStartEvent.mTimeStamp); ProcessEvent(aSwipeStartEvent, /* aProcessingFirstEvent = */ true); } void SwipeTracker::Destroy() { UnregisterFromRefreshDriver(); } SwipeTracker::~SwipeTracker() { MOZ_ASSERT(!mRegisteredWithRefreshDriver, "Destroy needs to be called before deallocating"); } double SwipeTracker::SwipeSuccessTargetValue() const { return (mSwipeDirection == dom::SimpleGestureEvent_Binding::DIRECTION_RIGHT) ? -1.0 : 1.0; } double SwipeTracker::ClampToAllowedRange(double aGestureAmount) const { // gestureAmount needs to stay between -1 and 0 when swiping right and // between 0 and 1 when swiping left. double min = (mSwipeDirection == dom::SimpleGestureEvent_Binding::DIRECTION_RIGHT) ? -1.0 : 0.0; double max = (mSwipeDirection == dom::SimpleGestureEvent_Binding::DIRECTION_LEFT) ? 1.0 : 0.0; return clamped(aGestureAmount, min, max); } bool SwipeTracker::ComputeSwipeSuccess() const { double targetValue = SwipeSuccessTargetValue(); // If the fingers were moving away from the target direction when they were // lifted from the touchpad, abort the swipe. if (mCurrentVelocity * targetValue < -StaticPrefs::widget_swipe_velocity_twitch_tolerance()) { return false; } return (mGestureAmount * targetValue + mCurrentVelocity * targetValue * StaticPrefs::widget_swipe_success_velocity_contribution()) >= kSwipeSuccessThreshold; } nsEventStatus SwipeTracker::ProcessEvent( const PanGestureInput& aEvent, bool aProcessingFirstEvent /* = false */) { // If the fingers have already been lifted or the swipe direction is where // navigation is impossible, don't process this event for swiping. if (!mEventsAreControllingSwipe || !SwipingInAllowedDirection()) { // Return nsEventStatus_eConsumeNoDefault for events from the swipe gesture // and nsEventStatus_eIgnore for events of subsequent scroll gestures. if (aEvent.mType == PanGestureInput::PANGESTURE_MAYSTART || aEvent.mType == PanGestureInput::PANGESTURE_START) { mEventsHaveStartedNewGesture = true; } return mEventsHaveStartedNewGesture ? nsEventStatus_eIgnore : nsEventStatus_eConsumeNoDefault; } double delta = -aEvent.mPanDisplacement.x / mWidget.GetDefaultScaleInternal() / StaticPrefs::widget_swipe_whole_page_pixel_size(); mGestureAmount = ClampToAllowedRange(mGestureAmount + delta); if (aEvent.mType != PanGestureInput::PANGESTURE_END) { if (!aProcessingFirstEvent) { double elapsedSeconds = std::max( 0.008, (aEvent.mTimeStamp - mLastEventTimeStamp).ToSeconds()); mCurrentVelocity = delta / elapsedSeconds; } mLastEventTimeStamp = aEvent.mTimeStamp; } const bool computedSwipeSuccess = ComputeSwipeSuccess(); // The velocity component might push us over the success threshold, in which // case we want to pass the success threshold in the event we send so that the // UI draws as 100% opacity to indicate such. We don't want to include the // velocity in the amount we put on the event if we aren't over the success // threshold because that would lead to the opacity decreasing even if the // user continues to increase the swipe distance. If we do compute swipe // success here and the user does not lift their fingers and then decreases // the total swipe so that we go below the success threshold the opacity would // also decrease in that case but that seems okay. // We don't want above tweak if we move the UI along with the opacity change // since it forces the UI element jump to the last position and jump back to // the original position if the navigation didn't happen. double eventAmount = mGestureAmount; if (computedSwipeSuccess && StaticPrefs::browser_swipe_navigation_icon_move_distance() == 0) { eventAmount = kSwipeSuccessThreshold; if (mGestureAmount < 0.f) { eventAmount = -eventAmount; } } // If ComputeSwipeSuccess returned false because the users fingers were moving // slightly away from the target direction then we do not want to display // the UI as if we were at the success threshold as that would give a false // indication that navigation would happen. if (!computedSwipeSuccess && (eventAmount >= kSwipeSuccessThreshold || eventAmount <= -kSwipeSuccessThreshold)) { eventAmount = 0.999 * kSwipeSuccessThreshold; if (mGestureAmount < 0.f) { eventAmount = -eventAmount; } } SendSwipeEvent(eSwipeGestureUpdate, 0, eventAmount, aEvent.mTimeStamp); if (aEvent.mType == PanGestureInput::PANGESTURE_END) { mEventsAreControllingSwipe = false; if (computedSwipeSuccess) { // Let's use same timestamp as previous event because this is caused by // the preceding event. SendSwipeEvent(eSwipeGesture, mSwipeDirection, 0.0, aEvent.mTimeStamp); UnregisterFromRefreshDriver(); NS_DispatchToMainThread( NS_NewRunnableFunction("SwipeTracker::SwipeFinished", [swipeTracker = RefPtr(this), timeStamp = aEvent.mTimeStamp] { swipeTracker->SwipeFinished(timeStamp); })); } else { StartAnimating(eventAmount, 0.0); } } return nsEventStatus_eConsumeNoDefault; } void SwipeTracker::StartAnimating(double aStartValue, double aTargetValue) { mAxis.SetPosition(aStartValue); mAxis.SetDestination(aTargetValue); mAxis.SetVelocity(mCurrentVelocity); mLastAnimationFrameTime = TimeStamp::Now(); // Add ourselves as a refresh driver observer. The refresh driver // will call WillRefresh for each animation frame until we // unregister ourselves. MOZ_ASSERT(!mRegisteredWithRefreshDriver); if (mRefreshDriver) { mRefreshDriver->AddRefreshObserver(this, FlushType::Style, "Swipe animation"); mRegisteredWithRefreshDriver = true; } } void SwipeTracker::WillRefresh(mozilla::TimeStamp aTime) { TimeStamp now = TimeStamp::Now(); mAxis.Simulate(now - mLastAnimationFrameTime); mLastAnimationFrameTime = now; bool isFinished = mAxis.IsFinished(1.0 / StaticPrefs::widget_swipe_whole_page_pixel_size()); mGestureAmount = (isFinished ? mAxis.GetDestination() : mAxis.GetPosition()); SendSwipeEvent(eSwipeGestureUpdate, 0, mGestureAmount, now); if (isFinished) { UnregisterFromRefreshDriver(); SwipeFinished(now); } } void SwipeTracker::CancelSwipe(const TimeStamp& aTimeStamp) { SendSwipeEvent(eSwipeGestureEnd, 0, 0.0, aTimeStamp); } void SwipeTracker::SwipeFinished(const TimeStamp& aTimeStamp) { SendSwipeEvent(eSwipeGestureEnd, 0, 0.0, aTimeStamp); mWidget.SwipeFinished(); } void SwipeTracker::UnregisterFromRefreshDriver() { if (mRegisteredWithRefreshDriver) { MOZ_ASSERT(mRefreshDriver, "How were we able to register, then?"); mRefreshDriver->RemoveRefreshObserver(this, FlushType::Style); } mRegisteredWithRefreshDriver = false; } /* static */ WidgetSimpleGestureEvent SwipeTracker::CreateSwipeGestureEvent( EventMessage aMsg, nsIWidget* aWidget, const LayoutDeviceIntPoint& aPosition, const TimeStamp& aTimeStamp) { // XXX Why isn't this initialized with nsCocoaUtils::InitInputEvent()? WidgetSimpleGestureEvent geckoEvent(true, aMsg, aWidget); geckoEvent.mModifiers = 0; // XXX How about geckoEvent.mTime? geckoEvent.mTimeStamp = aTimeStamp; geckoEvent.mRefPoint = aPosition; geckoEvent.mButtons = 0; return geckoEvent; } bool SwipeTracker::SendSwipeEvent(EventMessage aMsg, uint32_t aDirection, double aDelta, const TimeStamp& aTimeStamp) { WidgetSimpleGestureEvent geckoEvent = CreateSwipeGestureEvent(aMsg, &mWidget, mEventPosition, aTimeStamp); geckoEvent.mDirection = aDirection; geckoEvent.mDelta = aDelta; geckoEvent.mAllowedDirections = mAllowedDirections; return mWidget.DispatchWindowEvent(geckoEvent); } // static bool SwipeTracker::CanTriggerSwipe(const PanGestureInput& aPanInput) { if (StaticPrefs::widget_disable_swipe_tracker()) { return false; } if (aPanInput.mType != PanGestureInput::PANGESTURE_START) { return false; } // Only initiate horizontal tracking for events whose horizontal element is // at least eight times larger than its vertical element. This minimizes // performance problems with vertical scrolls (by minimizing the possibility // that they'll be misinterpreted as horizontal swipes), while still // tolerating a small vertical element to a true horizontal swipe. The number // '8' was arrived at by trial and error. return std::abs(aPanInput.mPanDisplacement.x) > std::abs(aPanInput.mPanDisplacement.y) * 8; } } // namespace mozilla