diff options
Diffstat (limited to 'widget/cocoa/SwipeTracker.mm')
-rw-r--r-- | widget/cocoa/SwipeTracker.mm | 219 |
1 files changed, 219 insertions, 0 deletions
diff --git a/widget/cocoa/SwipeTracker.mm b/widget/cocoa/SwipeTracker.mm new file mode 100644 index 0000000000..51169171a4 --- /dev/null +++ b/widget/cocoa/SwipeTracker.mm @@ -0,0 +1,219 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/TimeStamp.h" +#include "mozilla/TouchEvents.h" +#include "nsAlgorithm.h" +#include "nsChildView.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 kVelocityTwitchTolerance = 0.0000001; +static const double kWholePagePixelSize = 1000.0; +static const double kRubberBandResistanceFactor = 4.0; +static const double kSwipeSuccessThreshold = 0.25; +static const double kSwipeSuccessVelocityContribution = 0.3; + +namespace mozilla { + +static already_AddRefed<nsRefreshDriver> +GetRefreshDriver(nsIWidget& aWidget) +{ + nsIWidgetListener* widgetListener = aWidget.GetWidgetListener(); + nsIPresShell* presShell = widgetListener ? widgetListener->GetPresShell() : nullptr; + nsPresContext* presContext = presShell ? presShell->GetPresContext() : nullptr; + RefPtr<nsRefreshDriver> refreshDriver = presContext ? presContext->RefreshDriver() : nullptr; + return refreshDriver.forget(); +} + +SwipeTracker::SwipeTracker(nsChildView& 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<LayoutDevicePixel>(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); + ProcessEvent(aSwipeStartEvent); +} + +void +SwipeTracker::Destroy() +{ + UnregisterFromRefreshDriver(); +} + +SwipeTracker::~SwipeTracker() +{ + MOZ_ASSERT(!mRegisteredWithRefreshDriver, "Destroy needs to be called before deallocating"); +} + +double +SwipeTracker::SwipeSuccessTargetValue() const +{ + return (mSwipeDirection == nsIDOMSimpleGestureEvent::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 == nsIDOMSimpleGestureEvent::DIRECTION_RIGHT) ? -1.0 : 0.0; + double max = (mSwipeDirection == nsIDOMSimpleGestureEvent::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 < -kVelocityTwitchTolerance) { + return false; + } + + return (mGestureAmount * targetValue + + mCurrentVelocity * targetValue * kSwipeSuccessVelocityContribution) >= kSwipeSuccessThreshold; +} + +nsEventStatus +SwipeTracker::ProcessEvent(const PanGestureInput& aEvent) +{ + // If the fingers have already been lifted, don't process this event for swiping. + if (!mEventsAreControllingSwipe) { + // 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.BackingScaleFactor() / kWholePagePixelSize; + if (!SwipingInAllowedDirection()) { + delta /= kRubberBandResistanceFactor; + } + mGestureAmount = ClampToAllowedRange(mGestureAmount + delta); + SendSwipeEvent(eSwipeGestureUpdate, 0, mGestureAmount); + + if (aEvent.mType != PanGestureInput::PANGESTURE_END) { + double elapsedSeconds = std::max(0.008, (aEvent.mTimeStamp - mLastEventTimeStamp).ToSeconds()); + mCurrentVelocity = delta / elapsedSeconds; + mLastEventTimeStamp = aEvent.mTimeStamp; + } else { + mEventsAreControllingSwipe = false; + bool didSwipeSucceed = SwipingInAllowedDirection() && ComputeSwipeSuccess(); + double targetValue = 0.0; + if (didSwipeSucceed) { + SendSwipeEvent(eSwipeGesture, mSwipeDirection, 0.0); + targetValue = SwipeSuccessTargetValue(); + } + StartAnimating(targetValue); + } + + return nsEventStatus_eConsumeNoDefault; +} + +void +SwipeTracker::StartAnimating(double aTargetValue) +{ + mAxis.SetPosition(mGestureAmount); + 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, Flush_Style); + mRegisteredWithRefreshDriver = true; + } +} + +void +SwipeTracker::WillRefresh(mozilla::TimeStamp aTime) +{ + TimeStamp now = TimeStamp::Now(); + mAxis.Simulate(now - mLastAnimationFrameTime); + mLastAnimationFrameTime = now; + + bool isFinished = mAxis.IsFinished(1.0 / kWholePagePixelSize); + mGestureAmount = (isFinished ? mAxis.GetDestination() : mAxis.GetPosition()); + SendSwipeEvent(eSwipeGestureUpdate, 0, mGestureAmount); + + if (isFinished) { + UnregisterFromRefreshDriver(); + SwipeFinished(); + } +} + +void +SwipeTracker::CancelSwipe() +{ + SendSwipeEvent(eSwipeGestureEnd, 0, 0.0); +} + +void SwipeTracker::SwipeFinished() +{ + SendSwipeEvent(eSwipeGestureEnd, 0, 0.0); + mWidget.SwipeFinished(); +} + +void +SwipeTracker::UnregisterFromRefreshDriver() +{ + if (mRegisteredWithRefreshDriver) { + MOZ_ASSERT(mRefreshDriver, "How were we able to register, then?"); + mRefreshDriver->RemoveRefreshObserver(this, Flush_Style); + } + mRegisteredWithRefreshDriver = false; +} + +/* static */ WidgetSimpleGestureEvent +SwipeTracker::CreateSwipeGestureEvent(EventMessage aMsg, nsIWidget* aWidget, + const LayoutDeviceIntPoint& aPosition) +{ + WidgetSimpleGestureEvent geckoEvent(true, aMsg, aWidget); + geckoEvent.mModifiers = 0; + geckoEvent.mTimeStamp = TimeStamp::Now(); + geckoEvent.mRefPoint = aPosition; + geckoEvent.buttons = 0; + return geckoEvent; +} + +bool +SwipeTracker::SendSwipeEvent(EventMessage aMsg, uint32_t aDirection, double aDelta) +{ + WidgetSimpleGestureEvent geckoEvent = + CreateSwipeGestureEvent(aMsg, &mWidget, mEventPosition); + geckoEvent.mDirection = aDirection; + geckoEvent.mDelta = aDelta; + geckoEvent.mAllowedDirections = mAllowedDirections; + return mWidget.DispatchWindowEvent(geckoEvent); +} + +} // namespace mozilla |