summaryrefslogtreecommitdiffstats
path: root/widget/SwipeTracker.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--widget/SwipeTracker.cpp285
1 files changed, 285 insertions, 0 deletions
diff --git a/widget/SwipeTracker.cpp b/widget/SwipeTracker.cpp
new file mode 100644
index 0000000000..06800bcd64
--- /dev/null
+++ b/widget/SwipeTracker.cpp
@@ -0,0 +1,285 @@
+/* -*- 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<nsRefreshDriver> GetRefreshDriver(nsIWidget& aWidget) {
+ nsIWidgetListener* widgetListener = aWidget.GetWidgetListener();
+ PresShell* presShell =
+ widgetListener ? widgetListener->GetPresShell() : nullptr;
+ nsPresContext* presContext =
+ presShell ? presShell->GetPresContext() : nullptr;
+ RefPtr<nsRefreshDriver> 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<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, 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<SwipeTracker>(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