Bug 1942006 - Upstream a variety of Servo-specific code from Servo's downstream fork...
[gecko.git] / widget / SwipeTracker.cpp
blobb09252fd60beb10d5865d226c39ee0c8a9c22d87
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "SwipeTracker.h"
9 #include "InputData.h"
10 #include "mozilla/FlushType.h"
11 #include "mozilla/PresShell.h"
12 #include "mozilla/StaticPrefs_widget.h"
13 #include "mozilla/StaticPrefs_browser.h"
14 #include "mozilla/TimeStamp.h"
15 #include "mozilla/TouchEvents.h"
16 #include "mozilla/dom/SimpleGestureEventBinding.h"
17 #include "nsIWidget.h"
18 #include "nsRefreshDriver.h"
19 #include "UnitTransforms.h"
21 // These values were tweaked to make the physics feel similar to the native
22 // swipe.
23 static const double kSpringForce = 250.0;
24 static const double kSwipeSuccessThreshold = 0.25;
26 namespace mozilla {
28 static already_AddRefed<nsRefreshDriver> GetRefreshDriver(nsIWidget& aWidget) {
29 nsIWidgetListener* widgetListener = aWidget.GetWidgetListener();
30 PresShell* presShell =
31 widgetListener ? widgetListener->GetPresShell() : nullptr;
32 nsPresContext* presContext =
33 presShell ? presShell->GetPresContext() : nullptr;
34 RefPtr<nsRefreshDriver> refreshDriver =
35 presContext ? presContext->RefreshDriver() : nullptr;
36 return refreshDriver.forget();
39 SwipeTracker::SwipeTracker(nsIWidget& aWidget,
40 const PanGestureInput& aSwipeStartEvent,
41 uint32_t aAllowedDirections,
42 uint32_t aSwipeDirection)
43 : mWidget(aWidget),
44 mRefreshDriver(GetRefreshDriver(mWidget)),
45 mAxis(0.0, 0.0, 0.0, kSpringForce, 1.0),
46 mEventPosition(RoundedToInt(ViewAs<LayoutDevicePixel>(
47 aSwipeStartEvent.mPanStartPoint,
48 PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent))),
49 mLastEventTimeStamp(aSwipeStartEvent.mTimeStamp),
50 mAllowedDirections(aAllowedDirections),
51 mSwipeDirection(aSwipeDirection) {
52 SendSwipeEvent(eSwipeGestureStart, 0, 0.0, aSwipeStartEvent.mTimeStamp);
53 ProcessEvent(aSwipeStartEvent, /* aProcessingFirstEvent = */ true);
56 void SwipeTracker::Destroy() { UnregisterFromRefreshDriver(); }
58 SwipeTracker::~SwipeTracker() {
59 MOZ_RELEASE_ASSERT(!mRegisteredWithRefreshDriver,
60 "Destroy needs to be called before deallocating");
63 double SwipeTracker::SwipeSuccessTargetValue() const {
64 return mSwipeDirection == dom::SimpleGestureEvent_Binding::DIRECTION_RIGHT
65 ? -1.0
66 : 1.0;
69 double SwipeTracker::ClampToAllowedRange(double aGestureAmount) const {
70 // gestureAmount needs to stay between -1 and 0 when swiping right and
71 // between 0 and 1 when swiping left.
72 double min =
73 mSwipeDirection == dom::SimpleGestureEvent_Binding::DIRECTION_RIGHT ? -1.0
74 : 0.0;
75 double max =
76 mSwipeDirection == dom::SimpleGestureEvent_Binding::DIRECTION_LEFT ? 1.0
77 : 0.0;
78 return std::clamp(aGestureAmount, min, max);
81 bool SwipeTracker::ComputeSwipeSuccess() const {
82 double targetValue = SwipeSuccessTargetValue();
84 // If the fingers were moving away from the target direction when they were
85 // lifted from the touchpad, abort the swipe.
86 if (mCurrentVelocity * targetValue <
87 -StaticPrefs::widget_swipe_velocity_twitch_tolerance()) {
88 return false;
91 return (mGestureAmount * targetValue +
92 mCurrentVelocity * targetValue *
93 StaticPrefs::widget_swipe_success_velocity_contribution()) >=
94 kSwipeSuccessThreshold;
97 nsEventStatus SwipeTracker::ProcessEvent(
98 const PanGestureInput& aEvent, bool aProcessingFirstEvent /* = false */) {
99 // If the fingers have already been lifted or the swipe direction is where
100 // navigation is impossible, don't process this event for swiping.
101 if (!mEventsAreControllingSwipe || !SwipingInAllowedDirection()) {
102 // Return nsEventStatus_eConsumeNoDefault for events from the swipe gesture
103 // and nsEventStatus_eIgnore for events of subsequent scroll gestures.
104 if (aEvent.mType == PanGestureInput::PANGESTURE_MAYSTART ||
105 aEvent.mType == PanGestureInput::PANGESTURE_START) {
106 mEventsHaveStartedNewGesture = true;
108 return mEventsHaveStartedNewGesture ? nsEventStatus_eIgnore
109 : nsEventStatus_eConsumeNoDefault;
112 mDeltaTypeIsPage = aEvent.mDeltaType == PanGestureInput::PANDELTA_PAGE;
113 double delta = [&]() -> double {
114 if (mDeltaTypeIsPage) {
115 return -aEvent.mPanDisplacement.x / StaticPrefs::widget_swipe_page_size();
117 return -aEvent.mPanDisplacement.x / mWidget.GetDefaultScaleInternal() /
118 StaticPrefs::widget_swipe_pixel_size();
119 }();
121 mGestureAmount = ClampToAllowedRange(mGestureAmount + delta);
122 if (aEvent.mType != PanGestureInput::PANGESTURE_END) {
123 if (!aProcessingFirstEvent) {
124 double elapsedSeconds = std::max(
125 0.008, (aEvent.mTimeStamp - mLastEventTimeStamp).ToSeconds());
126 mCurrentVelocity = delta / elapsedSeconds;
128 mLastEventTimeStamp = aEvent.mTimeStamp;
131 const bool computedSwipeSuccess = ComputeSwipeSuccess();
132 double eventAmount = mGestureAmount;
133 // If ComputeSwipeSuccess returned false because the users fingers were
134 // moving slightly away from the target direction then we do not want to
135 // display the UI as if we were at the success threshold as that would
136 // give a false indication that navigation would happen.
137 if (!computedSwipeSuccess && (eventAmount >= kSwipeSuccessThreshold ||
138 eventAmount <= -kSwipeSuccessThreshold)) {
139 eventAmount = 0.999 * kSwipeSuccessThreshold;
140 if (mGestureAmount < 0.f) {
141 eventAmount = -eventAmount;
145 SendSwipeEvent(eSwipeGestureUpdate, 0, eventAmount, aEvent.mTimeStamp);
147 if (aEvent.mType == PanGestureInput::PANGESTURE_END) {
148 mEventsAreControllingSwipe = false;
149 if (computedSwipeSuccess) {
150 // Let's use same timestamp as previous event because this is caused by
151 // the preceding event.
152 SendSwipeEvent(eSwipeGesture, mSwipeDirection, 0.0, aEvent.mTimeStamp);
153 UnregisterFromRefreshDriver();
154 NS_DispatchToMainThread(
155 NS_NewRunnableFunction("SwipeTracker::SwipeFinished",
156 [swipeTracker = RefPtr<SwipeTracker>(this),
157 timeStamp = aEvent.mTimeStamp] {
158 swipeTracker->SwipeFinished(timeStamp);
159 }));
160 } else {
161 StartAnimating(eventAmount, 0.0);
165 return nsEventStatus_eConsumeNoDefault;
168 void SwipeTracker::StartAnimating(double aStartValue, double aTargetValue) {
169 mAxis.SetPosition(aStartValue);
170 mAxis.SetDestination(aTargetValue);
171 mAxis.SetVelocity(mCurrentVelocity);
173 mLastAnimationFrameTime = TimeStamp::Now();
175 // Add ourselves as a refresh driver observer. The refresh driver
176 // will call WillRefresh for each animation frame until we
177 // unregister ourselves.
178 MOZ_RELEASE_ASSERT(!mRegisteredWithRefreshDriver,
179 "We only want a single refresh driver registration");
180 if (mRefreshDriver) {
181 mRefreshDriver->AddRefreshObserver(this, FlushType::Style,
182 "Swipe animation");
183 mRegisteredWithRefreshDriver = true;
187 void SwipeTracker::WillRefresh(TimeStamp aTime) {
188 // FIXME(emilio): shouldn't we be using `aTime`?
189 TimeStamp now = TimeStamp::Now();
190 mAxis.Simulate(now - mLastAnimationFrameTime);
191 mLastAnimationFrameTime = now;
193 const double wholeSize = mDeltaTypeIsPage
194 ? StaticPrefs::widget_swipe_page_size()
195 : StaticPrefs::widget_swipe_pixel_size();
196 // NOTE(emilio): It's unclear this makes sense for page-based swiping, but
197 // this preserves behavior and all platforms probably will end up converging
198 // in pixel-based pan input, so...
199 const double minIncrement = 1.0 / wholeSize;
200 const bool isFinished = mAxis.IsFinished(minIncrement);
202 mGestureAmount = isFinished ? mAxis.GetDestination() : mAxis.GetPosition();
203 SendSwipeEvent(eSwipeGestureUpdate, 0, mGestureAmount, now);
205 if (isFinished) {
206 UnregisterFromRefreshDriver();
207 SwipeFinished(now);
211 void SwipeTracker::CancelSwipe(const TimeStamp& aTimeStamp) {
212 SendSwipeEvent(eSwipeGestureEnd, 0, 0.0, aTimeStamp);
215 void SwipeTracker::SwipeFinished(const TimeStamp& aTimeStamp) {
216 SendSwipeEvent(eSwipeGestureEnd, 0, 0.0, aTimeStamp);
217 mWidget.SwipeFinished();
220 void SwipeTracker::UnregisterFromRefreshDriver() {
221 if (mRegisteredWithRefreshDriver) {
222 MOZ_ASSERT(mRefreshDriver, "How were we able to register, then?");
223 mRefreshDriver->RemoveRefreshObserver(this, FlushType::Style);
224 mRegisteredWithRefreshDriver = false;
228 /* static */ WidgetSimpleGestureEvent SwipeTracker::CreateSwipeGestureEvent(
229 EventMessage aMsg, nsIWidget* aWidget,
230 const LayoutDeviceIntPoint& aPosition, const TimeStamp& aTimeStamp) {
231 // XXX Why isn't this initialized with nsCocoaUtils::InitInputEvent()?
232 WidgetSimpleGestureEvent geckoEvent(true, aMsg, aWidget);
233 geckoEvent.mModifiers = 0;
234 // XXX How about geckoEvent.mTime?
235 geckoEvent.mTimeStamp = aTimeStamp;
236 geckoEvent.mRefPoint = aPosition;
237 geckoEvent.mButtons = 0;
238 return geckoEvent;
241 bool SwipeTracker::SendSwipeEvent(EventMessage aMsg, uint32_t aDirection,
242 double aDelta, const TimeStamp& aTimeStamp) {
243 WidgetSimpleGestureEvent geckoEvent =
244 CreateSwipeGestureEvent(aMsg, &mWidget, mEventPosition, aTimeStamp);
245 geckoEvent.mDirection = aDirection;
246 geckoEvent.mDelta = aDelta;
247 geckoEvent.mAllowedDirections = mAllowedDirections;
248 return mWidget.DispatchWindowEvent(geckoEvent);
251 // static
252 bool SwipeTracker::CanTriggerSwipe(const PanGestureInput& aPanInput) {
253 if (StaticPrefs::widget_disable_swipe_tracker()) {
254 return false;
257 if (aPanInput.mType != PanGestureInput::PANGESTURE_START) {
258 return false;
261 // Only initiate horizontal tracking for events whose horizontal element is
262 // at least eight times larger than its vertical element. This minimizes
263 // performance problems with vertical scrolls (by minimizing the possibility
264 // that they'll be misinterpreted as horizontal swipes), while still
265 // tolerating a small vertical element to a true horizontal swipe. The number
266 // '8' was arrived at by trial and error.
267 return std::abs(aPanInput.mPanDisplacement.x) >
268 std::abs(aPanInput.mPanDisplacement.y) * 8;
271 } // namespace mozilla