SwipeTracker.cpp
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "SwipeTracker.h"
#include "InputData.h"
#include "mozilla/FlushType.h"
#include "mozilla/PresShell.h"
#include "mozilla/StaticPrefs_widget.h"
#include "mozilla/StaticPrefs_browser.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/TouchEvents.h"
#include "mozilla/dom/SimpleGestureEventBinding.h"
#include "nsAlgorithm.h"
#include "nsIWidget.h"
#include "nsRefreshDriver.h"
#include "UnitTransforms.h"
// These values were tweaked to make the physics feel similar to the native
// swipe.
static const double kSpringForce = 250.0;
static const double kSwipeSuccessThreshold = 0.25;
namespace mozilla {
static already_AddRefed<nsRefreshDriver> GetRefreshDriver(nsIWidget& aWidget) {
nsIWidgetListener* widgetListener = aWidget.GetWidgetListener();
PresShell* presShell =
widgetListener ? widgetListener->GetPresShell() : nullptr;
nsPresContext* presContext =
presShell ? presShell->GetPresContext() : nullptr;
RefPtr<nsRefreshDriver> refreshDriver =
presContext ? presContext->RefreshDriver() : nullptr;
return refreshDriver.forget();
}
SwipeTracker::SwipeTracker(nsIWidget& aWidget,
const PanGestureInput& aSwipeStartEvent,
uint32_t aAllowedDirections,
uint32_t aSwipeDirection)
: mWidget(aWidget),
mRefreshDriver(GetRefreshDriver(mWidget)),
mAxis(0.0, 0.0, 0.0, kSpringForce, 1.0),
mEventPosition(RoundedToInt(ViewAs<LayoutDevicePixel>(
aSwipeStartEvent.mPanStartPoint,
PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent))),
mLastEventTimeStamp(aSwipeStartEvent.mTimeStamp),
mAllowedDirections(aAllowedDirections),
mSwipeDirection(aSwipeDirection) {
SendSwipeEvent(eSwipeGestureStart, 0, 0.0, aSwipeStartEvent.mTimeStamp);
ProcessEvent(aSwipeStartEvent, /* aProcessingFirstEvent = */ true);
}
void SwipeTracker::Destroy() { UnregisterFromRefreshDriver(); }
SwipeTracker::~SwipeTracker() {
MOZ_RELEASE_ASSERT(!mRegisteredWithRefreshDriver,
"Destroy needs to be called before deallocating");
}
double SwipeTracker::SwipeSuccessTargetValue() const {
return mSwipeDirection == dom::SimpleGestureEvent_Binding::DIRECTION_RIGHT
? -1.0
: 1.0;
}
double SwipeTracker::ClampToAllowedRange(double aGestureAmount) const {
// gestureAmount needs to stay between -1 and 0 when swiping right and
// between 0 and 1 when swiping left.
double min =
mSwipeDirection == dom::SimpleGestureEvent_Binding::DIRECTION_RIGHT ? -1.0
: 0.0;
double max =
mSwipeDirection == dom::SimpleGestureEvent_Binding::DIRECTION_LEFT ? 1.0
: 0.0;
return clamped(aGestureAmount, min, max);
}
bool SwipeTracker::ComputeSwipeSuccess() const {
double targetValue = SwipeSuccessTargetValue();
// If the fingers were moving away from the target direction when they were
// lifted from the touchpad, abort the swipe.
if (mCurrentVelocity * targetValue <
-StaticPrefs::widget_swipe_velocity_twitch_tolerance()) {
return false;
}
return (mGestureAmount * targetValue +
mCurrentVelocity * targetValue *
StaticPrefs::widget_swipe_success_velocity_contribution()) >=
kSwipeSuccessThreshold;
}
nsEventStatus SwipeTracker::ProcessEvent(
const PanGestureInput& aEvent, bool aProcessingFirstEvent /* = false */) {
// If the fingers have already been lifted or the swipe direction is where
// navigation is impossible, don't process this event for swiping.
if (!mEventsAreControllingSwipe || !SwipingInAllowedDirection()) {
// Return nsEventStatus_eConsumeNoDefault for events from the swipe gesture
// and nsEventStatus_eIgnore for events of subsequent scroll gestures.
if (aEvent.mType == PanGestureInput::PANGESTURE_MAYSTART ||
aEvent.mType == PanGestureInput::PANGESTURE_START) {
mEventsHaveStartedNewGesture = true;
}
return mEventsHaveStartedNewGesture ? nsEventStatus_eIgnore
: nsEventStatus_eConsumeNoDefault;
}
mDeltaTypeIsPage = aEvent.mDeltaType == PanGestureInput::PANDELTA_PAGE;
double delta = [&]() -> double {
if (mDeltaTypeIsPage) {
return -aEvent.mPanDisplacement.x / StaticPrefs::widget_swipe_page_size();
}
return -aEvent.mPanDisplacement.x / mWidget.GetDefaultScaleInternal() /
StaticPrefs::widget_swipe_pixel_size();
}();
mGestureAmount = ClampToAllowedRange(mGestureAmount + delta);
if (aEvent.mType != PanGestureInput::PANGESTURE_END) {
if (!aProcessingFirstEvent) {
double elapsedSeconds = std::max(
0.008, (aEvent.mTimeStamp - mLastEventTimeStamp).ToSeconds());
mCurrentVelocity = delta / elapsedSeconds;
}
mLastEventTimeStamp = aEvent.mTimeStamp;
}
const bool computedSwipeSuccess = ComputeSwipeSuccess();
double eventAmount = mGestureAmount;
// If ComputeSwipeSuccess returned false because the users fingers were
// moving slightly away from the target direction then we do not want to
// display the UI as if we were at the success threshold as that would
// give a false indication that navigation would happen.
if (!computedSwipeSuccess && (eventAmount >= kSwipeSuccessThreshold ||
eventAmount <= -kSwipeSuccessThreshold)) {
eventAmount = 0.999 * kSwipeSuccessThreshold;
if (mGestureAmount < 0.f) {
eventAmount = -eventAmount;
}
}
SendSwipeEvent(eSwipeGestureUpdate, 0, eventAmount, aEvent.mTimeStamp);
if (aEvent.mType == PanGestureInput::PANGESTURE_END) {
mEventsAreControllingSwipe = false;
if (computedSwipeSuccess) {
// Let's use same timestamp as previous event because this is caused by
// the preceding event.
SendSwipeEvent(eSwipeGesture, mSwipeDirection, 0.0, aEvent.mTimeStamp);
UnregisterFromRefreshDriver();
NS_DispatchToMainThread(
NS_NewRunnableFunction("SwipeTracker::SwipeFinished",
[swipeTracker = RefPtr<SwipeTracker>(this),
timeStamp = aEvent.mTimeStamp] {
swipeTracker->SwipeFinished(timeStamp);
}));
} else {
StartAnimating(eventAmount, 0.0);
}
}
return nsEventStatus_eConsumeNoDefault;
}
void SwipeTracker::StartAnimating(double aStartValue, double aTargetValue) {
mAxis.SetPosition(aStartValue);
mAxis.SetDestination(aTargetValue);
mAxis.SetVelocity(mCurrentVelocity);
mLastAnimationFrameTime = TimeStamp::Now();
// Add ourselves as a refresh driver observer. The refresh driver
// will call WillRefresh for each animation frame until we
// unregister ourselves.
MOZ_RELEASE_ASSERT(!mRegisteredWithRefreshDriver,
"We only want a single refresh driver registration");
if (mRefreshDriver) {
mRefreshDriver->AddRefreshObserver(this, FlushType::Style,
"Swipe animation");
mRegisteredWithRefreshDriver = true;
}
}
void SwipeTracker::WillRefresh(TimeStamp aTime) {
// FIXME(emilio): shouldn't we be using `aTime`?
TimeStamp now = TimeStamp::Now();
mAxis.Simulate(now - mLastAnimationFrameTime);
mLastAnimationFrameTime = now;
const double wholeSize = mDeltaTypeIsPage
? StaticPrefs::widget_swipe_page_size()
: StaticPrefs::widget_swipe_pixel_size();
// NOTE(emilio): It's unclear this makes sense for page-based swiping, but
// this preserves behavior and all platforms probably will end up converging
// in pixel-based pan input, so...
const double minIncrement = 1.0 / wholeSize;
const bool isFinished = mAxis.IsFinished(minIncrement);
mGestureAmount = isFinished ? mAxis.GetDestination() : mAxis.GetPosition();
SendSwipeEvent(eSwipeGestureUpdate, 0, mGestureAmount, now);
if (isFinished) {
UnregisterFromRefreshDriver();
SwipeFinished(now);
}
}
void SwipeTracker::CancelSwipe(const TimeStamp& aTimeStamp) {
SendSwipeEvent(eSwipeGestureEnd, 0, 0.0, aTimeStamp);
}
void SwipeTracker::SwipeFinished(const TimeStamp& aTimeStamp) {
SendSwipeEvent(eSwipeGestureEnd, 0, 0.0, aTimeStamp);
mWidget.SwipeFinished();
}
void SwipeTracker::UnregisterFromRefreshDriver() {
if (mRegisteredWithRefreshDriver) {
MOZ_ASSERT(mRefreshDriver, "How were we able to register, then?");
mRefreshDriver->RemoveRefreshObserver(this, FlushType::Style);
mRegisteredWithRefreshDriver = false;
}
}
/* static */ WidgetSimpleGestureEvent SwipeTracker::CreateSwipeGestureEvent(
EventMessage aMsg, nsIWidget* aWidget,
const LayoutDeviceIntPoint& aPosition, const TimeStamp& aTimeStamp) {
// XXX Why isn't this initialized with nsCocoaUtils::InitInputEvent()?
WidgetSimpleGestureEvent geckoEvent(true, aMsg, aWidget);
geckoEvent.mModifiers = 0;
// XXX How about geckoEvent.mTime?
geckoEvent.mTimeStamp = aTimeStamp;
geckoEvent.mRefPoint = aPosition;
geckoEvent.mButtons = 0;
return geckoEvent;
}
bool SwipeTracker::SendSwipeEvent(EventMessage aMsg, uint32_t aDirection,
double aDelta, const TimeStamp& aTimeStamp) {
WidgetSimpleGestureEvent geckoEvent =
CreateSwipeGestureEvent(aMsg, &mWidget, mEventPosition, aTimeStamp);
geckoEvent.mDirection = aDirection;
geckoEvent.mDelta = aDelta;
geckoEvent.mAllowedDirections = mAllowedDirections;
return mWidget.DispatchWindowEvent(geckoEvent);
}
// static
bool SwipeTracker::CanTriggerSwipe(const PanGestureInput& aPanInput) {
if (StaticPrefs::widget_disable_swipe_tracker()) {
return false;
}
if (aPanInput.mType != PanGestureInput::PANGESTURE_START) {
return false;
}
// Only initiate horizontal tracking for events whose horizontal element is
// at least eight times larger than its vertical element. This minimizes
// performance problems with vertical scrolls (by minimizing the possibility
// that they'll be misinterpreted as horizontal swipes), while still
// tolerating a small vertical element to a true horizontal swipe. The number
// '8' was arrived at by trial and error.
return std::abs(aPanInput.mPanDisplacement.x) >
std::abs(aPanInput.mPanDisplacement.y) * 8;
}
} // namespace mozilla