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"
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 "nsAlgorithm.h"
18 #include "nsIWidget.h"
19 #include "nsRefreshDriver.h"
20 #include "UnitTransforms.h"
22 // These values were tweaked to make the physics feel similar to the native
24 static const double kSpringForce
= 250.0;
25 static const double kSwipeSuccessThreshold
= 0.25;
29 static already_AddRefed
<nsRefreshDriver
> GetRefreshDriver(nsIWidget
& aWidget
) {
30 nsIWidgetListener
* widgetListener
= aWidget
.GetWidgetListener();
31 PresShell
* presShell
=
32 widgetListener
? widgetListener
->GetPresShell() : nullptr;
33 nsPresContext
* presContext
=
34 presShell
? presShell
->GetPresContext() : nullptr;
35 RefPtr
<nsRefreshDriver
> refreshDriver
=
36 presContext
? presContext
->RefreshDriver() : nullptr;
37 return refreshDriver
.forget();
40 SwipeTracker::SwipeTracker(nsIWidget
& aWidget
,
41 const PanGestureInput
& aSwipeStartEvent
,
42 uint32_t aAllowedDirections
,
43 uint32_t aSwipeDirection
)
45 mRefreshDriver(GetRefreshDriver(mWidget
)),
46 mAxis(0.0, 0.0, 0.0, kSpringForce
, 1.0),
47 mEventPosition(RoundedToInt(ViewAs
<LayoutDevicePixel
>(
48 aSwipeStartEvent
.mPanStartPoint
,
49 PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent
))),
50 mLastEventTimeStamp(aSwipeStartEvent
.mTimeStamp
),
51 mAllowedDirections(aAllowedDirections
),
52 mSwipeDirection(aSwipeDirection
) {
53 SendSwipeEvent(eSwipeGestureStart
, 0, 0.0, aSwipeStartEvent
.mTimeStamp
);
54 ProcessEvent(aSwipeStartEvent
, /* aProcessingFirstEvent = */ true);
57 void SwipeTracker::Destroy() { UnregisterFromRefreshDriver(); }
59 SwipeTracker::~SwipeTracker() {
60 MOZ_RELEASE_ASSERT(!mRegisteredWithRefreshDriver
,
61 "Destroy needs to be called before deallocating");
64 double SwipeTracker::SwipeSuccessTargetValue() const {
65 return mSwipeDirection
== dom::SimpleGestureEvent_Binding::DIRECTION_RIGHT
70 double SwipeTracker::ClampToAllowedRange(double aGestureAmount
) const {
71 // gestureAmount needs to stay between -1 and 0 when swiping right and
72 // between 0 and 1 when swiping left.
74 mSwipeDirection
== dom::SimpleGestureEvent_Binding::DIRECTION_RIGHT
? -1.0
77 mSwipeDirection
== dom::SimpleGestureEvent_Binding::DIRECTION_LEFT
? 1.0
79 return clamped(aGestureAmount
, min
, max
);
82 bool SwipeTracker::ComputeSwipeSuccess() const {
83 double targetValue
= SwipeSuccessTargetValue();
85 // If the fingers were moving away from the target direction when they were
86 // lifted from the touchpad, abort the swipe.
87 if (mCurrentVelocity
* targetValue
<
88 -StaticPrefs::widget_swipe_velocity_twitch_tolerance()) {
92 return (mGestureAmount
* targetValue
+
93 mCurrentVelocity
* targetValue
*
94 StaticPrefs::widget_swipe_success_velocity_contribution()) >=
95 kSwipeSuccessThreshold
;
98 nsEventStatus
SwipeTracker::ProcessEvent(
99 const PanGestureInput
& aEvent
, bool aProcessingFirstEvent
/* = false */) {
100 // If the fingers have already been lifted or the swipe direction is where
101 // navigation is impossible, don't process this event for swiping.
102 if (!mEventsAreControllingSwipe
|| !SwipingInAllowedDirection()) {
103 // Return nsEventStatus_eConsumeNoDefault for events from the swipe gesture
104 // and nsEventStatus_eIgnore for events of subsequent scroll gestures.
105 if (aEvent
.mType
== PanGestureInput::PANGESTURE_MAYSTART
||
106 aEvent
.mType
== PanGestureInput::PANGESTURE_START
) {
107 mEventsHaveStartedNewGesture
= true;
109 return mEventsHaveStartedNewGesture
? nsEventStatus_eIgnore
110 : nsEventStatus_eConsumeNoDefault
;
113 mDeltaTypeIsPage
= aEvent
.mDeltaType
== PanGestureInput::PANDELTA_PAGE
;
114 double delta
= [&]() -> double {
115 if (mDeltaTypeIsPage
) {
116 return -aEvent
.mPanDisplacement
.x
/ StaticPrefs::widget_swipe_page_size();
118 return -aEvent
.mPanDisplacement
.x
/ mWidget
.GetDefaultScaleInternal() /
119 StaticPrefs::widget_swipe_pixel_size();
122 mGestureAmount
= ClampToAllowedRange(mGestureAmount
+ delta
);
123 if (aEvent
.mType
!= PanGestureInput::PANGESTURE_END
) {
124 if (!aProcessingFirstEvent
) {
125 double elapsedSeconds
= std::max(
126 0.008, (aEvent
.mTimeStamp
- mLastEventTimeStamp
).ToSeconds());
127 mCurrentVelocity
= delta
/ elapsedSeconds
;
129 mLastEventTimeStamp
= aEvent
.mTimeStamp
;
132 const bool computedSwipeSuccess
= ComputeSwipeSuccess();
133 double eventAmount
= mGestureAmount
;
134 // If ComputeSwipeSuccess returned false because the users fingers were
135 // moving slightly away from the target direction then we do not want to
136 // display the UI as if we were at the success threshold as that would
137 // give a false indication that navigation would happen.
138 if (!computedSwipeSuccess
&& (eventAmount
>= kSwipeSuccessThreshold
||
139 eventAmount
<= -kSwipeSuccessThreshold
)) {
140 eventAmount
= 0.999 * kSwipeSuccessThreshold
;
141 if (mGestureAmount
< 0.f
) {
142 eventAmount
= -eventAmount
;
146 SendSwipeEvent(eSwipeGestureUpdate
, 0, eventAmount
, aEvent
.mTimeStamp
);
148 if (aEvent
.mType
== PanGestureInput::PANGESTURE_END
) {
149 mEventsAreControllingSwipe
= false;
150 if (computedSwipeSuccess
) {
151 // Let's use same timestamp as previous event because this is caused by
152 // the preceding event.
153 SendSwipeEvent(eSwipeGesture
, mSwipeDirection
, 0.0, aEvent
.mTimeStamp
);
154 UnregisterFromRefreshDriver();
155 NS_DispatchToMainThread(
156 NS_NewRunnableFunction("SwipeTracker::SwipeFinished",
157 [swipeTracker
= RefPtr
<SwipeTracker
>(this),
158 timeStamp
= aEvent
.mTimeStamp
] {
159 swipeTracker
->SwipeFinished(timeStamp
);
162 StartAnimating(eventAmount
, 0.0);
166 return nsEventStatus_eConsumeNoDefault
;
169 void SwipeTracker::StartAnimating(double aStartValue
, double aTargetValue
) {
170 mAxis
.SetPosition(aStartValue
);
171 mAxis
.SetDestination(aTargetValue
);
172 mAxis
.SetVelocity(mCurrentVelocity
);
174 mLastAnimationFrameTime
= TimeStamp::Now();
176 // Add ourselves as a refresh driver observer. The refresh driver
177 // will call WillRefresh for each animation frame until we
178 // unregister ourselves.
179 MOZ_RELEASE_ASSERT(!mRegisteredWithRefreshDriver
,
180 "We only want a single refresh driver registration");
181 if (mRefreshDriver
) {
182 mRefreshDriver
->AddRefreshObserver(this, FlushType::Style
,
184 mRegisteredWithRefreshDriver
= true;
188 void SwipeTracker::WillRefresh(TimeStamp aTime
) {
189 // FIXME(emilio): shouldn't we be using `aTime`?
190 TimeStamp now
= TimeStamp::Now();
191 mAxis
.Simulate(now
- mLastAnimationFrameTime
);
192 mLastAnimationFrameTime
= now
;
194 const double wholeSize
= mDeltaTypeIsPage
195 ? StaticPrefs::widget_swipe_page_size()
196 : StaticPrefs::widget_swipe_pixel_size();
197 // NOTE(emilio): It's unclear this makes sense for page-based swiping, but
198 // this preserves behavior and all platforms probably will end up converging
199 // in pixel-based pan input, so...
200 const double minIncrement
= 1.0 / wholeSize
;
201 const bool isFinished
= mAxis
.IsFinished(minIncrement
);
203 mGestureAmount
= isFinished
? mAxis
.GetDestination() : mAxis
.GetPosition();
204 SendSwipeEvent(eSwipeGestureUpdate
, 0, mGestureAmount
, now
);
207 UnregisterFromRefreshDriver();
212 void SwipeTracker::CancelSwipe(const TimeStamp
& aTimeStamp
) {
213 SendSwipeEvent(eSwipeGestureEnd
, 0, 0.0, aTimeStamp
);
216 void SwipeTracker::SwipeFinished(const TimeStamp
& aTimeStamp
) {
217 SendSwipeEvent(eSwipeGestureEnd
, 0, 0.0, aTimeStamp
);
218 mWidget
.SwipeFinished();
221 void SwipeTracker::UnregisterFromRefreshDriver() {
222 if (mRegisteredWithRefreshDriver
) {
223 MOZ_ASSERT(mRefreshDriver
, "How were we able to register, then?");
224 mRefreshDriver
->RemoveRefreshObserver(this, FlushType::Style
);
225 mRegisteredWithRefreshDriver
= false;
229 /* static */ WidgetSimpleGestureEvent
SwipeTracker::CreateSwipeGestureEvent(
230 EventMessage aMsg
, nsIWidget
* aWidget
,
231 const LayoutDeviceIntPoint
& aPosition
, const TimeStamp
& aTimeStamp
) {
232 // XXX Why isn't this initialized with nsCocoaUtils::InitInputEvent()?
233 WidgetSimpleGestureEvent
geckoEvent(true, aMsg
, aWidget
);
234 geckoEvent
.mModifiers
= 0;
235 // XXX How about geckoEvent.mTime?
236 geckoEvent
.mTimeStamp
= aTimeStamp
;
237 geckoEvent
.mRefPoint
= aPosition
;
238 geckoEvent
.mButtons
= 0;
242 bool SwipeTracker::SendSwipeEvent(EventMessage aMsg
, uint32_t aDirection
,
243 double aDelta
, const TimeStamp
& aTimeStamp
) {
244 WidgetSimpleGestureEvent geckoEvent
=
245 CreateSwipeGestureEvent(aMsg
, &mWidget
, mEventPosition
, aTimeStamp
);
246 geckoEvent
.mDirection
= aDirection
;
247 geckoEvent
.mDelta
= aDelta
;
248 geckoEvent
.mAllowedDirections
= mAllowedDirections
;
249 return mWidget
.DispatchWindowEvent(geckoEvent
);
253 bool SwipeTracker::CanTriggerSwipe(const PanGestureInput
& aPanInput
) {
254 if (StaticPrefs::widget_disable_swipe_tracker()) {
258 if (aPanInput
.mType
!= PanGestureInput::PANGESTURE_START
) {
262 // Only initiate horizontal tracking for events whose horizontal element is
263 // at least eight times larger than its vertical element. This minimizes
264 // performance problems with vertical scrolls (by minimizing the possibility
265 // that they'll be misinterpreted as horizontal swipes), while still
266 // tolerating a small vertical element to a true horizontal swipe. The number
267 // '8' was arrived at by trial and error.
268 return std::abs(aPanInput
.mPanDisplacement
.x
) >
269 std::abs(aPanInput
.mPanDisplacement
.y
) * 8;
272 } // namespace mozilla