/* -*- 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) { SendSwipeEvent(eSwipeGestureStart, 0, 0.0, aSwipeStartEvent.mTimeStamp); ProcessEvent(aSwipeStartEvent, /* aProcessingFirstEvent = */ true); } void SwipeTracker::Destroy() { UnregisterFromRefreshDriver(); } SwipeTracker::~SwipeTracker() { MOZ_RELEASE_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; } mDeltaTypeIsPage = aEvent.mDeltaType == PanGestureInput::PANDELTA_PAGE; double delta = [&]() -> double { if (mDeltaTypeIsPage) { return -aEvent.mPanDisplacement.x / StaticPrefs::widget_swipe_page_size(); } return -aEvent.mPanDisplacement.x / mWidget.GetDefaultScaleInternal() / StaticPrefs::widget_swipe_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(); double eventAmount = mGestureAmount; // 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_RELEASE_ASSERT(!mRegisteredWithRefreshDriver, "We only want a single refresh driver registration"); if (mRefreshDriver) { mRefreshDriver->AddRefreshObserver(this, FlushType::Style, "Swipe animation"); mRegisteredWithRefreshDriver = true; } } void SwipeTracker::WillRefresh(TimeStamp aTime) { // FIXME(emilio): shouldn't we be using `aTime`? TimeStamp now = TimeStamp::Now(); mAxis.Simulate(now - mLastAnimationFrameTime); mLastAnimationFrameTime = now; const double wholeSize = mDeltaTypeIsPage ? StaticPrefs::widget_swipe_page_size() : StaticPrefs::widget_swipe_pixel_size(); // NOTE(emilio): It's unclear this makes sense for page-based swiping, but // this preserves behavior and all platforms probably will end up converging // in pixel-based pan input, so... const double minIncrement = 1.0 / wholeSize; const bool isFinished = mAxis.IsFinished(minIncrement); 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