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/. */
6
7#include "SwipeTracker.h"
8
9#include "InputData.h"
10#include "mozilla/FlushType.h"
11#include "mozilla/PresShell.h"
12#include "mozilla/TimeStamp.h"
13#include "mozilla/TouchEvents.h"
14#include "mozilla/dom/SimpleGestureEventBinding.h"
15#include "nsAlgorithm.h"
16#include "nsChildView.h"
17#include "nsRefreshDriver.h"
18#include "UnitTransforms.h"
19
20// These values were tweaked to make the physics feel similar to the native swipe.
21static const double kSpringForce = 250.0;
22static const double kVelocityTwitchTolerance = 0.0000001;
23static const double kWholePagePixelSize = 550.0;
24static const double kRubberBandResistanceFactor = 4.0;
25static const double kSwipeSuccessThreshold = 0.25;
26static const double kSwipeSuccessVelocityContribution = 0.05;
27
28namespace mozilla {
29
30static already_AddRefed<nsRefreshDriver> GetRefreshDriver(nsIWidget& aWidget) {
31  nsIWidgetListener* widgetListener = aWidget.GetWidgetListener();
32  PresShell* presShell = widgetListener ? widgetListener->GetPresShell() : nullptr;
33  nsPresContext* presContext = presShell ? presShell->GetPresContext() : nullptr;
34  RefPtr<nsRefreshDriver> refreshDriver = presContext ? presContext->RefreshDriver() : nullptr;
35  return refreshDriver.forget();
36}
37
38SwipeTracker::SwipeTracker(nsChildView& aWidget, const PanGestureInput& aSwipeStartEvent,
39                           uint32_t aAllowedDirections, uint32_t aSwipeDirection)
40    : mWidget(aWidget),
41      mRefreshDriver(GetRefreshDriver(mWidget)),
42      mAxis(0.0, 0.0, 0.0, kSpringForce, 1.0),
43      mEventPosition(RoundedToInt(ViewAs<LayoutDevicePixel>(
44          aSwipeStartEvent.mPanStartPoint,
45          PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent))),
46      mLastEventTimeStamp(aSwipeStartEvent.mTimeStamp),
47      mAllowedDirections(aAllowedDirections),
48      mSwipeDirection(aSwipeDirection),
49      mGestureAmount(0.0),
50      mCurrentVelocity(0.0),
51      mEventsAreControllingSwipe(true),
52      mEventsHaveStartedNewGesture(false),
53      mRegisteredWithRefreshDriver(false) {
54  SendSwipeEvent(eSwipeGestureStart, 0, 0.0, aSwipeStartEvent.mTimeStamp);
55  ProcessEvent(aSwipeStartEvent);
56}
57
58void SwipeTracker::Destroy() { UnregisterFromRefreshDriver(); }
59
60SwipeTracker::~SwipeTracker() {
61  MOZ_ASSERT(!mRegisteredWithRefreshDriver, "Destroy needs to be called before deallocating");
62}
63
64double SwipeTracker::SwipeSuccessTargetValue() const {
65  return (mSwipeDirection == dom::SimpleGestureEvent_Binding::DIRECTION_RIGHT) ? -1.0 : 1.0;
66}
67
68double SwipeTracker::ClampToAllowedRange(double aGestureAmount) const {
69  // gestureAmount needs to stay between -1 and 0 when swiping right and
70  // between 0 and 1 when swiping left.
71  double min = (mSwipeDirection == dom::SimpleGestureEvent_Binding::DIRECTION_RIGHT) ? -1.0 : 0.0;
72  double max = (mSwipeDirection == dom::SimpleGestureEvent_Binding::DIRECTION_LEFT) ? 1.0 : 0.0;
73  return clamped(aGestureAmount, min, max);
74}
75
76bool SwipeTracker::ComputeSwipeSuccess() const {
77  double targetValue = SwipeSuccessTargetValue();
78
79  // If the fingers were moving away from the target direction when they were
80  // lifted from the touchpad, abort the swipe.
81  if (mCurrentVelocity * targetValue < -kVelocityTwitchTolerance) {
82    return false;
83  }
84
85  return (mGestureAmount * targetValue +
86          mCurrentVelocity * targetValue * kSwipeSuccessVelocityContribution) >=
87         kSwipeSuccessThreshold;
88}
89
90nsEventStatus SwipeTracker::ProcessEvent(const PanGestureInput& aEvent) {
91  // If the fingers have already been lifted or the swipe direction is where
92  // navigation is impossible, don't process this event for swiping.
93  if (!mEventsAreControllingSwipe || !SwipingInAllowedDirection()) {
94    // Return nsEventStatus_eConsumeNoDefault for events from the swipe gesture
95    // and nsEventStatus_eIgnore for events of subsequent scroll gestures.
96    if (aEvent.mType == PanGestureInput::PANGESTURE_MAYSTART ||
97        aEvent.mType == PanGestureInput::PANGESTURE_START) {
98      mEventsHaveStartedNewGesture = true;
99    }
100    return mEventsHaveStartedNewGesture ? nsEventStatus_eIgnore : nsEventStatus_eConsumeNoDefault;
101  }
102
103  double delta = -aEvent.mPanDisplacement.x / mWidget.BackingScaleFactor() / kWholePagePixelSize;
104  mGestureAmount = ClampToAllowedRange(mGestureAmount + delta);
105  SendSwipeEvent(eSwipeGestureUpdate, 0, mGestureAmount, aEvent.mTimeStamp);
106
107  if (aEvent.mType != PanGestureInput::PANGESTURE_END) {
108    double elapsedSeconds = std::max(0.008, (aEvent.mTimeStamp - mLastEventTimeStamp).ToSeconds());
109    mCurrentVelocity = delta / elapsedSeconds;
110    mLastEventTimeStamp = aEvent.mTimeStamp;
111  } else {
112    mEventsAreControllingSwipe = false;
113    double targetValue = 0.0;
114    if (ComputeSwipeSuccess()) {
115      // Let's use same timestamp as previous event because this is caused by
116      // the preceding event.
117      SendSwipeEvent(eSwipeGesture, mSwipeDirection, 0.0, aEvent.mTimeStamp);
118      targetValue = SwipeSuccessTargetValue();
119    }
120    StartAnimating(targetValue);
121  }
122
123  return nsEventStatus_eConsumeNoDefault;
124}
125
126void SwipeTracker::StartAnimating(double aTargetValue) {
127  mAxis.SetPosition(mGestureAmount);
128  mAxis.SetDestination(aTargetValue);
129  mAxis.SetVelocity(mCurrentVelocity);
130
131  mLastAnimationFrameTime = TimeStamp::Now();
132
133  // Add ourselves as a refresh driver observer. The refresh driver
134  // will call WillRefresh for each animation frame until we
135  // unregister ourselves.
136  MOZ_ASSERT(!mRegisteredWithRefreshDriver);
137  if (mRefreshDriver) {
138    mRefreshDriver->AddRefreshObserver(this, FlushType::Style, "Swipe animation");
139    mRegisteredWithRefreshDriver = true;
140  }
141}
142
143void SwipeTracker::WillRefresh(mozilla::TimeStamp aTime) {
144  TimeStamp now = TimeStamp::Now();
145  mAxis.Simulate(now - mLastAnimationFrameTime);
146  mLastAnimationFrameTime = now;
147
148  bool isFinished = mAxis.IsFinished(1.0 / kWholePagePixelSize);
149  mGestureAmount = (isFinished ? mAxis.GetDestination() : mAxis.GetPosition());
150  SendSwipeEvent(eSwipeGestureUpdate, 0, mGestureAmount, now);
151
152  if (isFinished) {
153    UnregisterFromRefreshDriver();
154    SwipeFinished(now);
155  }
156}
157
158void SwipeTracker::CancelSwipe(const TimeStamp& aTimeStamp) {
159  SendSwipeEvent(eSwipeGestureEnd, 0, 0.0, aTimeStamp);
160}
161
162void SwipeTracker::SwipeFinished(const TimeStamp& aTimeStamp) {
163  SendSwipeEvent(eSwipeGestureEnd, 0, 0.0, aTimeStamp);
164  mWidget.SwipeFinished();
165}
166
167void SwipeTracker::UnregisterFromRefreshDriver() {
168  if (mRegisteredWithRefreshDriver) {
169    MOZ_ASSERT(mRefreshDriver, "How were we able to register, then?");
170    mRefreshDriver->RemoveRefreshObserver(this, FlushType::Style);
171  }
172  mRegisteredWithRefreshDriver = false;
173}
174
175/* static */ WidgetSimpleGestureEvent SwipeTracker::CreateSwipeGestureEvent(
176    EventMessage aMsg, nsIWidget* aWidget, const LayoutDeviceIntPoint& aPosition,
177    const TimeStamp& aTimeStamp) {
178  // XXX Why isn't this initialized with nsCocoaUtils::InitInputEvent()?
179  WidgetSimpleGestureEvent geckoEvent(true, aMsg, aWidget);
180  geckoEvent.mModifiers = 0;
181  // XXX How about geckoEvent.mTime?
182  geckoEvent.mTimeStamp = aTimeStamp;
183  geckoEvent.mRefPoint = aPosition;
184  geckoEvent.mButtons = 0;
185  return geckoEvent;
186}
187
188bool SwipeTracker::SendSwipeEvent(EventMessage aMsg, uint32_t aDirection, double aDelta,
189                                  const TimeStamp& aTimeStamp) {
190  WidgetSimpleGestureEvent geckoEvent =
191      CreateSwipeGestureEvent(aMsg, &mWidget, mEventPosition, aTimeStamp);
192  geckoEvent.mDirection = aDirection;
193  geckoEvent.mDelta = aDelta;
194  geckoEvent.mAllowedDirections = mAllowedDirections;
195  return mWidget.DispatchWindowEvent(geckoEvent);
196}
197
198}  // namespace mozilla
199