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