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
),
54 mCurrentVelocity(0.0),
55 mEventsAreControllingSwipe(true),
56 mEventsHaveStartedNewGesture(false),
57 mRegisteredWithRefreshDriver(false) {
58 SendSwipeEvent(eSwipeGestureStart
, 0, 0.0, aSwipeStartEvent
.mTimeStamp
);
59 ProcessEvent(aSwipeStartEvent
, /* aProcessingFirstEvent = */ true);
62 void SwipeTracker::Destroy() { UnregisterFromRefreshDriver(); }
64 SwipeTracker::~SwipeTracker() {
65 MOZ_ASSERT(!mRegisteredWithRefreshDriver
,
66 "Destroy needs to be called before deallocating");
69 double SwipeTracker::SwipeSuccessTargetValue() const {
70 return (mSwipeDirection
== dom::SimpleGestureEvent_Binding::DIRECTION_RIGHT
)
75 double SwipeTracker::ClampToAllowedRange(double aGestureAmount
) const {
76 // gestureAmount needs to stay between -1 and 0 when swiping right and
77 // between 0 and 1 when swiping left.
79 (mSwipeDirection
== dom::SimpleGestureEvent_Binding::DIRECTION_RIGHT
)
83 (mSwipeDirection
== dom::SimpleGestureEvent_Binding::DIRECTION_LEFT
)
86 return clamped(aGestureAmount
, min
, max
);
89 bool SwipeTracker::ComputeSwipeSuccess() const {
90 double targetValue
= SwipeSuccessTargetValue();
92 // If the fingers were moving away from the target direction when they were
93 // lifted from the touchpad, abort the swipe.
94 if (mCurrentVelocity
* targetValue
<
95 -StaticPrefs::widget_swipe_velocity_twitch_tolerance()) {
99 return (mGestureAmount
* targetValue
+
100 mCurrentVelocity
* targetValue
*
101 StaticPrefs::widget_swipe_success_velocity_contribution()) >=
102 kSwipeSuccessThreshold
;
105 nsEventStatus
SwipeTracker::ProcessEvent(
106 const PanGestureInput
& aEvent
, bool aProcessingFirstEvent
/* = false */) {
107 // If the fingers have already been lifted or the swipe direction is where
108 // navigation is impossible, don't process this event for swiping.
109 if (!mEventsAreControllingSwipe
|| !SwipingInAllowedDirection()) {
110 // Return nsEventStatus_eConsumeNoDefault for events from the swipe gesture
111 // and nsEventStatus_eIgnore for events of subsequent scroll gestures.
112 if (aEvent
.mType
== PanGestureInput::PANGESTURE_MAYSTART
||
113 aEvent
.mType
== PanGestureInput::PANGESTURE_START
) {
114 mEventsHaveStartedNewGesture
= true;
116 return mEventsHaveStartedNewGesture
? nsEventStatus_eIgnore
117 : nsEventStatus_eConsumeNoDefault
;
120 double delta
= -aEvent
.mPanDisplacement
.x
/
121 mWidget
.GetDefaultScaleInternal() /
122 StaticPrefs::widget_swipe_whole_page_pixel_size();
123 mGestureAmount
= ClampToAllowedRange(mGestureAmount
+ delta
);
124 if (aEvent
.mType
!= PanGestureInput::PANGESTURE_END
) {
125 if (!aProcessingFirstEvent
) {
126 double elapsedSeconds
= std::max(
127 0.008, (aEvent
.mTimeStamp
- mLastEventTimeStamp
).ToSeconds());
128 mCurrentVelocity
= delta
/ elapsedSeconds
;
130 mLastEventTimeStamp
= aEvent
.mTimeStamp
;
133 const bool computedSwipeSuccess
= ComputeSwipeSuccess();
134 double eventAmount
= mGestureAmount
;
135 // If ComputeSwipeSuccess returned false because the users fingers were
136 // moving slightly away from the target direction then we do not want to
137 // display the UI as if we were at the success threshold as that would
138 // give a false indication that navigation would happen.
139 if (!computedSwipeSuccess
&& (eventAmount
>= kSwipeSuccessThreshold
||
140 eventAmount
<= -kSwipeSuccessThreshold
)) {
141 eventAmount
= 0.999 * kSwipeSuccessThreshold
;
142 if (mGestureAmount
< 0.f
) {
143 eventAmount
= -eventAmount
;
147 SendSwipeEvent(eSwipeGestureUpdate
, 0, eventAmount
, aEvent
.mTimeStamp
);
149 if (aEvent
.mType
== PanGestureInput::PANGESTURE_END
) {
150 mEventsAreControllingSwipe
= false;
151 if (computedSwipeSuccess
) {
152 // Let's use same timestamp as previous event because this is caused by
153 // the preceding event.
154 SendSwipeEvent(eSwipeGesture
, mSwipeDirection
, 0.0, aEvent
.mTimeStamp
);
155 UnregisterFromRefreshDriver();
156 NS_DispatchToMainThread(
157 NS_NewRunnableFunction("SwipeTracker::SwipeFinished",
158 [swipeTracker
= RefPtr
<SwipeTracker
>(this),
159 timeStamp
= aEvent
.mTimeStamp
] {
160 swipeTracker
->SwipeFinished(timeStamp
);
163 StartAnimating(eventAmount
, 0.0);
167 return nsEventStatus_eConsumeNoDefault
;
170 void SwipeTracker::StartAnimating(double aStartValue
, double aTargetValue
) {
171 mAxis
.SetPosition(aStartValue
);
172 mAxis
.SetDestination(aTargetValue
);
173 mAxis
.SetVelocity(mCurrentVelocity
);
175 mLastAnimationFrameTime
= TimeStamp::Now();
177 // Add ourselves as a refresh driver observer. The refresh driver
178 // will call WillRefresh for each animation frame until we
179 // unregister ourselves.
180 MOZ_ASSERT(!mRegisteredWithRefreshDriver
);
181 if (mRefreshDriver
) {
182 mRefreshDriver
->AddRefreshObserver(this, FlushType::Style
,
184 mRegisteredWithRefreshDriver
= true;
188 void SwipeTracker::WillRefresh(mozilla::TimeStamp aTime
) {
189 TimeStamp now
= TimeStamp::Now();
190 mAxis
.Simulate(now
- mLastAnimationFrameTime
);
191 mLastAnimationFrameTime
= now
;
194 mAxis
.IsFinished(1.0 / StaticPrefs::widget_swipe_whole_page_pixel_size());
195 mGestureAmount
= (isFinished
? mAxis
.GetDestination() : mAxis
.GetPosition());
196 SendSwipeEvent(eSwipeGestureUpdate
, 0, mGestureAmount
, now
);
199 UnregisterFromRefreshDriver();
204 void SwipeTracker::CancelSwipe(const TimeStamp
& aTimeStamp
) {
205 SendSwipeEvent(eSwipeGestureEnd
, 0, 0.0, aTimeStamp
);
208 void SwipeTracker::SwipeFinished(const TimeStamp
& aTimeStamp
) {
209 SendSwipeEvent(eSwipeGestureEnd
, 0, 0.0, aTimeStamp
);
210 mWidget
.SwipeFinished();
213 void SwipeTracker::UnregisterFromRefreshDriver() {
214 if (mRegisteredWithRefreshDriver
) {
215 MOZ_ASSERT(mRefreshDriver
, "How were we able to register, then?");
216 mRefreshDriver
->RemoveRefreshObserver(this, FlushType::Style
);
218 mRegisteredWithRefreshDriver
= false;
221 /* static */ WidgetSimpleGestureEvent
SwipeTracker::CreateSwipeGestureEvent(
222 EventMessage aMsg
, nsIWidget
* aWidget
,
223 const LayoutDeviceIntPoint
& aPosition
, const TimeStamp
& aTimeStamp
) {
224 // XXX Why isn't this initialized with nsCocoaUtils::InitInputEvent()?
225 WidgetSimpleGestureEvent
geckoEvent(true, aMsg
, aWidget
);
226 geckoEvent
.mModifiers
= 0;
227 // XXX How about geckoEvent.mTime?
228 geckoEvent
.mTimeStamp
= aTimeStamp
;
229 geckoEvent
.mRefPoint
= aPosition
;
230 geckoEvent
.mButtons
= 0;
234 bool SwipeTracker::SendSwipeEvent(EventMessage aMsg
, uint32_t aDirection
,
235 double aDelta
, const TimeStamp
& aTimeStamp
) {
236 WidgetSimpleGestureEvent geckoEvent
=
237 CreateSwipeGestureEvent(aMsg
, &mWidget
, mEventPosition
, aTimeStamp
);
238 geckoEvent
.mDirection
= aDirection
;
239 geckoEvent
.mDelta
= aDelta
;
240 geckoEvent
.mAllowedDirections
= mAllowedDirections
;
241 return mWidget
.DispatchWindowEvent(geckoEvent
);
245 bool SwipeTracker::CanTriggerSwipe(const PanGestureInput
& aPanInput
) {
246 if (StaticPrefs::widget_disable_swipe_tracker()) {
250 if (aPanInput
.mType
!= PanGestureInput::PANGESTURE_START
) {
254 // Only initiate horizontal tracking for events whose horizontal element is
255 // at least eight times larger than its vertical element. This minimizes
256 // performance problems with vertical scrolls (by minimizing the possibility
257 // that they'll be misinterpreted as horizontal swipes), while still
258 // tolerating a small vertical element to a true horizontal swipe. The number
259 // '8' was arrived at by trial and error.
260 return std::abs(aPanInput
.mPanDisplacement
.x
) >
261 std::abs(aPanInput
.mPanDisplacement
.y
) * 8;
264 } // namespace mozilla