Bug 1016035 - Implement the swipe animation ourselves instead of calling the NSEvent trackSwipe API. r=kats
authorMarkus Stange <mstange@themasta.com>
Thu, 27 Aug 2015 16:07:59 -0400
changeset 259975 e118d7c25839ac6e2f61cf8d18cc03004fc4cd72
parent 259974 5d4380c90c053a1d42b5b8eaef0bdd2273ddb02d
child 259976 f4dcb289cf3ca49ae506c99b560a15796f1b34e4
push id29296
push userryanvm@gmail.com
push dateSun, 30 Aug 2015 19:45:10 +0000
treeherdermozilla-central@2ad5077d86ba [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskats
bugs1016035
milestone43.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1016035 - Implement the swipe animation ourselves instead of calling the NSEvent trackSwipe API. r=kats
widget/cocoa/SwipeTracker.h
widget/cocoa/SwipeTracker.mm
widget/cocoa/moz.build
widget/cocoa/nsChildView.h
widget/cocoa/nsChildView.mm
new file mode 100644
--- /dev/null
+++ b/widget/cocoa/SwipeTracker.h
@@ -0,0 +1,96 @@
+/* -*- 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/. */
+
+#ifndef SwipeTracker_h
+#define SwipeTracker_h
+
+#include "EventForwards.h"
+#include "mozilla/layers/AxisPhysicsMSDModel.h"
+#include "mozilla/nsRefPtr.h"
+#include "mozilla/TimeStamp.h"
+#include "nsRefreshDriver.h"
+#include "Units.h"
+
+class nsIPresShell;
+
+namespace mozilla {
+
+class PanGestureInput;
+
+/**
+ * SwipeTracker turns PanGestureInput events into swipe events
+ * (WidgetSimpleGestureEvent) and dispatches them into Gecko.
+ * The swiping behavior mirrors the behavior of the Cocoa API
+ * -[NSEvent trackSwipeEventWithOptions:dampenAmountThresholdMin:max:usingHandler:].
+ * The advantage of using this class over the Cocoa API is that this class
+ * properly supports submitting queued up events to it, and that it hopefully
+ * doesn't intermittently break scrolling the way the Cocoa API does (bug 927702).
+ *
+ * The swipe direction is either left or right. It is determined before the
+ * SwipeTracker is created and stays fixed during the swipe.
+ * During the swipe, the swipe has a current "value" which is between 0 and the
+ * target value. The target value is either 1 (swiping left) or -1 (swiping
+ * right) - see SwipeSuccessTargetValue().
+ * A swipe can either succeed or fail. If it succeeds, the swipe animation
+ * animates towards the success target value; if it fails, it animates back to
+ * a value of 0. A swipe can only succeed if the user is swiping in an allowed
+ * direction. (Since both the allowed directions and the swipe direction are
+ * known at swipe start time, it's clear from the beginning whether a swipe is
+ * doomed to fail. In that case, the purpose of the SwipeTracker is to simulate
+ * a bounce-back animation.)
+ */
+class SwipeTracker final : public nsARefreshObserver {
+public:
+  NS_INLINE_DECL_REFCOUNTING(SwipeTracker, override)
+
+  SwipeTracker(nsChildView& aWidget,
+               const PanGestureInput& aSwipeStartEvent,
+               uint32_t aAllowedDirections,
+               uint32_t aSwipeDirection);
+
+  void Destroy();
+
+  nsEventStatus ProcessEvent(const PanGestureInput& aEvent);
+  void CancelSwipe();
+
+  static WidgetSimpleGestureEvent
+    CreateSwipeGestureEvent(EventMessage aMsg, nsIWidget* aWidget,
+                            const LayoutDeviceIntPoint& aPosition);
+
+
+  // nsARefreshObserver
+  void WillRefresh(mozilla::TimeStamp aTime) override;
+
+protected:
+  ~SwipeTracker();
+
+  bool SwipingInAllowedDirection() const { return mAllowedDirections & mSwipeDirection; }
+  double SwipeSuccessTargetValue() const;
+  double ClampToAllowedRange(double aGestureAmount) const;
+  bool ComputeSwipeSuccess() const;
+  void StartAnimating(double aTargetValue);
+  void SwipeFinished();
+  void UnregisterFromRefreshDriver();
+  bool SendSwipeEvent(EventMessage aMsg, uint32_t aDirection, double aDelta);
+
+  nsChildView& mWidget;
+  nsRefPtr<nsRefreshDriver> mRefreshDriver;
+  layers::AxisPhysicsMSDModel mAxis;
+  const LayoutDeviceIntPoint mEventPosition;
+  TimeStamp mLastEventTimeStamp;
+  TimeStamp mLastAnimationFrameTime;
+  const uint32_t mAllowedDirections;
+  const uint32_t mSwipeDirection;
+  double mGestureAmount;
+  double mCurrentVelocity;
+  bool mEventsAreControllingSwipe;
+  bool mEventsHaveStartedNewGesture;
+  bool mRegisteredWithRefreshDriver;
+};
+
+} // namespace mozilla
+
+#endif // SwipeTracker_h
new file mode 100644
--- /dev/null
+++ b/widget/cocoa/SwipeTracker.mm
@@ -0,0 +1,219 @@
+/* -*- 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/TimeStamp.h"
+#include "mozilla/TouchEvents.h"
+#include "nsAlgorithm.h"
+#include "nsChildView.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 kVelocityTwitchTolerance = 0.0000001;
+static const double kWholePagePixelSize = 1000.0;
+static const double kRubberBandResistanceFactor = 4.0;
+static const double kSwipeSuccessThreshold = 0.25;
+static const double kSwipeSuccessVelocityContribution = 0.3;
+
+namespace mozilla {
+
+static already_AddRefed<nsRefreshDriver>
+GetRefreshDriver(nsIWidget& aWidget)
+{
+  nsIWidgetListener* widgetListener = aWidget.GetWidgetListener();
+  nsIPresShell* presShell = widgetListener ? widgetListener->GetPresShell() : nullptr;
+  nsPresContext* presContext = presShell ? presShell->GetPresContext() : nullptr;
+  nsRefPtr<nsRefreshDriver> refreshDriver = presContext ? presContext->RefreshDriver() : nullptr;
+  return refreshDriver.forget();
+}
+
+SwipeTracker::SwipeTracker(nsChildView& 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::LayoutDeviceToScreenForUntransformedEvent)))
+  , mLastEventTimeStamp(aSwipeStartEvent.mTimeStamp)
+  , mAllowedDirections(aAllowedDirections)
+  , mSwipeDirection(aSwipeDirection)
+  , mGestureAmount(0.0)
+  , mCurrentVelocity(0.0)
+  , mEventsAreControllingSwipe(true)
+  , mEventsHaveStartedNewGesture(false)
+  , mRegisteredWithRefreshDriver(false)
+{
+  ProcessEvent(aSwipeStartEvent);
+}
+
+void
+SwipeTracker::Destroy()
+{
+  UnregisterFromRefreshDriver();
+}
+
+SwipeTracker::~SwipeTracker()
+{
+  MOZ_ASSERT(!mRegisteredWithRefreshDriver, "Destroy needs to be called before deallocating");
+}
+
+double
+SwipeTracker::SwipeSuccessTargetValue() const
+{
+  return (mSwipeDirection == nsIDOMSimpleGestureEvent::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 == nsIDOMSimpleGestureEvent::DIRECTION_RIGHT) ? -1.0 : 0.0;
+  double max = (mSwipeDirection == nsIDOMSimpleGestureEvent::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 < -kVelocityTwitchTolerance) {
+    return false;
+  }
+
+  return (mGestureAmount * targetValue +
+          mCurrentVelocity * targetValue * kSwipeSuccessVelocityContribution) >= kSwipeSuccessThreshold;
+}
+
+nsEventStatus
+SwipeTracker::ProcessEvent(const PanGestureInput& aEvent)
+{
+  // If the fingers have already been lifted, don't process this event for swiping.
+  if (!mEventsAreControllingSwipe) {
+    // 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;
+  }
+
+  double delta = -aEvent.mPanDisplacement.x / mWidget.BackingScaleFactor() / kWholePagePixelSize;
+  if (!SwipingInAllowedDirection()) {
+    delta /= kRubberBandResistanceFactor;
+  }
+  mGestureAmount = ClampToAllowedRange(mGestureAmount + delta);
+  SendSwipeEvent(NS_SIMPLE_GESTURE_SWIPE_UPDATE, 0, mGestureAmount);
+
+  if (aEvent.mType != PanGestureInput::PANGESTURE_END) {
+    double elapsedSeconds = std::max(0.008, (aEvent.mTimeStamp - mLastEventTimeStamp).ToSeconds());
+    mCurrentVelocity = delta / elapsedSeconds;
+    mLastEventTimeStamp = aEvent.mTimeStamp;
+  } else {
+    mEventsAreControllingSwipe = false;
+    bool didSwipeSucceed = SwipingInAllowedDirection() && ComputeSwipeSuccess();
+    double targetValue = 0.0;
+    if (didSwipeSucceed) {
+      SendSwipeEvent(NS_SIMPLE_GESTURE_SWIPE, mSwipeDirection, 0.0);
+      targetValue = SwipeSuccessTargetValue();
+    }
+    StartAnimating(targetValue);
+  }
+
+  return nsEventStatus_eConsumeNoDefault;
+}
+
+void
+SwipeTracker::StartAnimating(double aTargetValue)
+{
+  mAxis.SetPosition(mGestureAmount);
+  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_ASSERT(!mRegisteredWithRefreshDriver);
+  if (mRefreshDriver) {
+    mRefreshDriver->AddRefreshObserver(this, Flush_Style);
+    mRegisteredWithRefreshDriver = true;
+  }
+}
+
+void
+SwipeTracker::WillRefresh(mozilla::TimeStamp aTime)
+{
+  TimeStamp now = TimeStamp::Now();
+  mAxis.Simulate(now - mLastAnimationFrameTime);
+  mLastAnimationFrameTime = now;
+
+  bool isFinished = mAxis.IsFinished(1.0 / kWholePagePixelSize);
+  mGestureAmount = (isFinished ? mAxis.GetDestination() : mAxis.GetPosition());
+  SendSwipeEvent(NS_SIMPLE_GESTURE_SWIPE_UPDATE, 0, mGestureAmount);
+
+  if (isFinished) {
+    UnregisterFromRefreshDriver();
+    SwipeFinished();
+  }
+}
+
+void
+SwipeTracker::CancelSwipe()
+{
+  SendSwipeEvent(NS_SIMPLE_GESTURE_SWIPE_END, 0, 0.0);
+}
+
+void SwipeTracker::SwipeFinished()
+{
+  SendSwipeEvent(NS_SIMPLE_GESTURE_SWIPE_END, 0, 0.0);
+  mWidget.SwipeFinished();
+}
+
+void
+SwipeTracker::UnregisterFromRefreshDriver()
+{
+  if (mRegisteredWithRefreshDriver) {
+    MOZ_ASSERT(mRefreshDriver, "How were we able to register, then?");
+    mRefreshDriver->RemoveRefreshObserver(this, Flush_Style);
+  }
+  mRegisteredWithRefreshDriver = false;
+}
+
+/* static */ WidgetSimpleGestureEvent
+SwipeTracker::CreateSwipeGestureEvent(EventMessage aMsg, nsIWidget* aWidget,
+                                      const LayoutDeviceIntPoint& aPosition)
+{
+  WidgetSimpleGestureEvent geckoEvent(true, aMsg, aWidget);
+  geckoEvent.modifiers = 0;
+  geckoEvent.timeStamp = TimeStamp::Now();
+  geckoEvent.refPoint = aPosition;
+  geckoEvent.buttons = 0;
+  return geckoEvent;
+}
+
+bool
+SwipeTracker::SendSwipeEvent(EventMessage aMsg, uint32_t aDirection, double aDelta)
+{
+  WidgetSimpleGestureEvent geckoEvent =
+    CreateSwipeGestureEvent(aMsg, &mWidget, mEventPosition);
+  geckoEvent.direction = aDirection;
+  geckoEvent.delta = aDelta;
+  geckoEvent.allowedDirections = mAllowedDirections;
+  return mWidget.DispatchWindowEvent(geckoEvent);
+}
+
+} // namespace mozilla
--- a/widget/cocoa/moz.build
+++ b/widget/cocoa/moz.build
@@ -26,52 +26,53 @@ UNIFIED_SOURCES += [
     'nsAppShell.mm',
     'nsBidiKeyboard.mm',
     'nsCocoaFeatures.mm',
     'nsCocoaUtils.mm',
     'nsCocoaWindow.mm',
     'nsColorPicker.mm',
     'nsCursorManager.mm',
     'nsDeviceContextSpecX.mm',
-    'nsDragService.mm',
     'nsFilePicker.mm',
     'nsIdleServiceX.mm',
     'nsLookAndFeel.mm',
     'nsMacCursor.mm',
     'nsMacDockSupport.mm',
     'nsMacWebAppUtils.mm',
     'nsMenuBarX.mm',
     'nsMenuGroupOwnerX.mm',
     'nsMenuItemIconX.mm',
     'nsMenuItemX.mm',
     'nsMenuUtilsX.mm',
     'nsMenuX.mm',
-    'nsNativeThemeCocoa.mm',
     'nsPrintDialogX.mm',
     'nsPrintOptionsX.mm',
     'nsPrintSettingsX.mm',
     'nsScreenCocoa.mm',
     'nsScreenManagerCocoa.mm',
     'nsSound.mm',
     'nsStandaloneNativeMenu.mm',
     'nsSystemStatusBarCocoa.mm',
     'nsToolkit.mm',
     'nsWidgetFactory.mm',
     'nsWindowMap.mm',
     'OSXNotificationCenter.mm',
+    'SwipeTracker.mm',
     'TextInputHandler.mm',
     'VibrancyManager.mm',
     'WidgetTraceEvent.mm',
 ]
 
 # These files cannot be built in unified mode because they cause symbol conflicts
 SOURCES += [
     'nsChildView.mm',
     'nsClipboard.mm',
     'nsCocoaDebugUtils.mm',
+    'nsDragService.mm',
+    'nsNativeThemeCocoa.mm',
 ]
 
 if not CONFIG['RELEASE_BUILD'] or CONFIG['DEBUG']:
     SOURCES += [
         'nsSandboxViolationSink.mm',
     ]
 
 include('/ipc/chromium/chromium-config.mozbuild')
--- a/widget/cocoa/nsChildView.h
+++ b/widget/cocoa/nsChildView.h
@@ -37,18 +37,20 @@ class nsChildView;
 class nsCocoaWindow;
 
 namespace {
 class GLPresenter;
 class RectTextureImage;
 } // namespace
 
 namespace mozilla {
+class InputData;
+class PanGestureInput;
+class SwipeTracker;
 class VibrancyManager;
-class InputData;
 namespace layers {
 class GLManager;
 class APZCTreeManager;
 } // namespace layers
 } // namespace mozilla
 
 @interface NSEvent (Undocumented)
 
@@ -303,21 +305,16 @@ typedef NSInteger NSEventGestureAxis;
 - (void)endGestureWithEvent:(NSEvent *)anEvent;
 
 - (void)scrollWheel:(NSEvent *)anEvent;
 - (void)handleAsyncScrollEvent:(CGEventRef)cgEvent ofType:(CGEventType)type;
 
 // Helper function for Lion smart magnify events
 + (BOOL)isLionSmartMagnifyEvent:(NSEvent*)anEvent;
 
-// Support for fluid swipe tracking.
-#ifdef __LP64__
-- (void)maybeTrackScrollEventAsSwipe:(NSEvent *)anEvent;
-#endif
-
 - (void)setUsingOMTCompositor:(BOOL)aUseOMTC;
 
 - (NSEvent*)lastKeyDownEvent;
 @end
 
 class ChildViewMouseTracker {
 
 public:
@@ -550,16 +547,20 @@ public:
   NS_IMETHOD SetPluginFocused(bool& aFocused) override;
 
   bool IsPluginFocused() { return mPluginFocused; }
 
   virtual nsIntPoint GetClientOffset() override;
 
   mozilla::WidgetWheelEvent DispatchAPZWheelInputEvent(mozilla::InputData& aEvent);
 
+  mozilla::SwipeTracker* GetSwipeTracker() { return mSwipeTracker.get(); }
+  void MaybeTrackScrollEventAsSwipe(const mozilla::PanGestureInput& aSwipeStartEvent);
+  void SwipeFinished();
+
 protected:
   virtual ~nsChildView();
 
   void              ReportMoveEvent();
   void              ReportSizeEvent();
 
   // override to create different kinds of child views. Autoreleases, so
   // caller must retain.
@@ -658,15 +659,16 @@ protected:
 
   bool mPluginFocused;
 
   // Used in OMTC BasicLayers mode. Presents the BasicCompositor result
   // surface to the screen using an OpenGL context.
   nsAutoPtr<GLPresenter> mGLPresenter;
 
   mozilla::UniquePtr<mozilla::VibrancyManager> mVibrancyManager;
+  nsRefPtr<mozilla::SwipeTracker> mSwipeTracker;
 
   static uint32_t sLastInputEventCount;
 
   void ReleaseTitlebarCGContext();
 };
 
 #endif // nsChildView_h_
--- a/widget/cocoa/nsChildView.mm
+++ b/widget/cocoa/nsChildView.mm
@@ -84,16 +84,17 @@
 #include <ApplicationServices/ApplicationServices.h>
 
 #include "GeckoProfiler.h"
 
 #include "nsIDOMWheelEvent.h"
 #include "mozilla/layers/ChromeProcessController.h"
 #include "nsLayoutUtils.h"
 #include "InputData.h"
+#include "SwipeTracker.h"
 #include "VibrancyManager.h"
 #include "nsNativeThemeCocoa.h"
 #include "nsIDOMWindowUtils.h"
 
 using namespace mozilla;
 using namespace mozilla::layers;
 using namespace mozilla::gl;
 using namespace mozilla::widget;
@@ -392,16 +393,21 @@ nsChildView::nsChildView() : nsBaseWidge
 {
   EnsureLogInitialized();
 }
 
 nsChildView::~nsChildView()
 {
   ReleaseTitlebarCGContext();
 
+  if (mSwipeTracker) {
+    mSwipeTracker->Destroy();
+    mSwipeTracker = nullptr;
+  }
+
   // Notify the children that we're gone.  childView->ResetParent() can change
   // our list of children while it's being iterated, so the way we iterate the
   // list must allow for this.
   for (nsIWidget* kid = mLastChild; kid;) {
     nsChildView* childView = static_cast<nsChildView*>(kid);
     kid = kid->GetPrevSibling();
     childView->ResetParent();
   }
@@ -2559,16 +2565,59 @@ nsChildView::EnsureVibrancyManager()
 {
   MOZ_ASSERT(mView, "Only call this once we have a view!");
   if (!mVibrancyManager) {
     mVibrancyManager = MakeUnique<VibrancyManager>(*this, mView);
   }
   return *mVibrancyManager;
 }
 
+void
+nsChildView::MaybeTrackScrollEventAsSwipe(const mozilla::PanGestureInput& aSwipeStartEvent)
+{
+  nsCOMPtr<nsIWidget> kungFuDeathGrip(this);
+
+  uint32_t direction = (aSwipeStartEvent.mPanDisplacement.x > 0.0)
+    ? (uint32_t)nsIDOMSimpleGestureEvent::DIRECTION_RIGHT
+    : (uint32_t)nsIDOMSimpleGestureEvent::DIRECTION_LEFT;
+
+  // We're ready to start the animation. Tell Gecko about it, and at the same
+  // time ask it if it really wants to start an animation for this event.
+  // This event also reports back the directions that we can swipe in.
+  LayoutDeviceIntPoint position =
+    RoundedToInt(aSwipeStartEvent.mPanStartPoint * ScreenToLayoutDeviceScale(1));
+  WidgetSimpleGestureEvent geckoEvent =
+    SwipeTracker::CreateSwipeGestureEvent(NS_SIMPLE_GESTURE_SWIPE_START, this, position);
+  geckoEvent.direction = direction;
+  geckoEvent.delta = 0.0;
+  geckoEvent.allowedDirections = 0;
+  bool shouldStartSwipe = DispatchWindowEvent(geckoEvent); // event cancelled == swipe should start
+
+  if (!shouldStartSwipe) {
+    return;
+  }
+
+  // If a swipe is currently being tracked kill it -- it's been interrupted
+  // by another gesture event.
+  if (mSwipeTracker) {
+    mSwipeTracker->CancelSwipe();
+    mSwipeTracker->Destroy();
+    mSwipeTracker = nullptr;
+  }
+
+  mSwipeTracker = new SwipeTracker(*this, aSwipeStartEvent,
+                                   geckoEvent.allowedDirections, direction);
+}
+
+void
+nsChildView::SwipeFinished()
+{
+  mSwipeTracker = nullptr;
+}
+
 already_AddRefed<gfx::DrawTarget>
 nsChildView::StartRemoteDrawing()
 {
   // should have created the GLPresenter in InitCompositor.
   MOZ_ASSERT(mGLPresenter);
   if (!mGLPresenter) {
     mGLPresenter = GLPresenter::CreateForWindow(this);
 
@@ -2654,16 +2703,17 @@ nsChildView::UpdateWindowDraggingRegion(
     [(ChildView*)mView updateWindowDraggableState];
   }
 }
 
 WidgetWheelEvent
 nsChildView::DispatchAPZWheelInputEvent(InputData& aEvent)
 {
   WidgetWheelEvent event(true, NS_WHEEL_WHEEL, this);
+
   if (mAPZC) {
     uint64_t inputBlockId = 0;
     ScrollableLayerGuid guid;
 
     nsEventStatus result = mAPZC->ReceiveInputEvent(aEvent, &guid, &inputBlockId);
     if (result == nsEventStatus_eConsumeNoDefault) {
       return event;
     }
@@ -4245,48 +4295,16 @@ NSEvent* gLastDragMouseDownEvent = nil;
    * 0x16, but it will probably never change. See bug 863841.
    */
   return nsCocoaFeatures::OnLionOrLater() &&
          !nsCocoaFeatures::OnMountainLionOrLater() &&
          [anEvent type] == NSEventTypeGesture &&
          [anEvent subtype] == 0x16;
 }
 
-#ifdef __LP64__
-- (bool)sendSwipeEvent:(NSEvent*)aEvent
-                withKind:(EventMessage)aMsg
-       allowedDirections:(uint32_t*)aAllowedDirections
-               direction:(uint32_t)aDirection
-                   delta:(double)aDelta
-{
-  if (!mGeckoChild)
-    return false;
-
-  WidgetSimpleGestureEvent geckoEvent(true, aMsg, mGeckoChild);
-  geckoEvent.direction = aDirection;
-  geckoEvent.delta = aDelta;
-  geckoEvent.allowedDirections = *aAllowedDirections;
-  [self convertCocoaMouseEvent:aEvent toGeckoEvent:&geckoEvent];
-  bool eventCancelled = mGeckoChild->DispatchWindowEvent(geckoEvent);
-  *aAllowedDirections = geckoEvent.allowedDirections;
-  return eventCancelled; // event cancelled == swipe should start
-}
-
-- (void)sendSwipeEndEvent:(NSEvent *)anEvent
-        allowedDirections:(uint32_t)aAllowedDirections
-{
-    // Tear down animation overlay by sending a swipe end event.
-    uint32_t allowedDirectionsCopy = aAllowedDirections;
-    [self sendSwipeEvent:anEvent
-                withKind:NS_SIMPLE_GESTURE_SWIPE_END
-       allowedDirections:&allowedDirectionsCopy
-               direction:0
-                   delta:0.0];
-}
-
 - (bool)shouldConsiderStartingSwipeFromEvent:(NSEvent*)anEvent
 {
   if (!nsCocoaFeatures::OnLionOrLater()) {
     return false;
   }
 
   // This method checks whether the AppleEnableSwipeNavigateWithScrolls global
   // preference is set.  If it isn't, fluid swipe tracking is disabled, and a
@@ -4313,142 +4331,16 @@ NSEvent* gLastDragMouseDownEvent = nil;
   // 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.
   CGFloat deltaX = [anEvent scrollingDeltaX];
   CGFloat deltaY = [anEvent scrollingDeltaY];
   return std::abs(deltaX) > std::abs(deltaY) * 8;
 }
 
-// Support fluid swipe tracking on OS X 10.7 and higher. We must be careful
-// to only invoke this support on a two-finger gesture that really
-// is a swipe (and not a scroll) -- in other words, the app is responsible
-// for deciding which is which. But once the decision is made, the OS tracks
-// the swipe until it has finished, and decides whether or not it succeeded.
-// A horizontal swipe has the same functionality as the Back and Forward
-// buttons.
-// This method is partly based on Apple sample code available at
-// developer.apple.com/library/mac/#releasenotes/Cocoa/AppKitOlderNotes.html
-// (under Fluid Swipe Tracking API).
-- (void)maybeTrackScrollEventAsSwipe:(NSEvent *)anEvent
-{
-  CGFloat deltaX = [anEvent scrollingDeltaX];
-
-  uint32_t direction = (deltaX < 0.0)
-    ? (uint32_t)nsIDOMSimpleGestureEvent::DIRECTION_RIGHT
-    : (uint32_t)nsIDOMSimpleGestureEvent::DIRECTION_LEFT;
-
-  // We're ready to start the animation. Tell Gecko about it, and at the same
-  // time ask it if it really wants to start an animation for this event.
-  // This event also reports back the directions that we can swipe in.
-  uint32_t allowedDirections = 0;
-  bool shouldStartSwipe = [self sendSwipeEvent:anEvent
-                                      withKind:NS_SIMPLE_GESTURE_SWIPE_START
-                             allowedDirections:&allowedDirections
-                                     direction:direction
-                                         delta:0.0];
-
-  if (!shouldStartSwipe) {
-    return;
-  }
-
-  // If a swipe is currently being tracked kill it -- it's been interrupted
-  // by another gesture event.
-  if (mCancelSwipeAnimation && *mCancelSwipeAnimation == NO) {
-    *mCancelSwipeAnimation = YES;
-    mCancelSwipeAnimation = nil;
-  }
-
-  CGFloat min = (allowedDirections & nsIDOMSimpleGestureEvent::DIRECTION_RIGHT) ? -1.0 : 0.0;
-  CGFloat max = (allowedDirections & nsIDOMSimpleGestureEvent::DIRECTION_LEFT) ? 1.0 : 0.0;
-
-  __block BOOL animationCanceled = NO;
-  __block BOOL geckoSwipeEventSent = NO;
-  // At this point, anEvent is the first scroll wheel event in a two-finger
-  // horizontal gesture that we've decided to treat as a swipe.  When we call
-  // [NSEvent trackSwipeEventWithOptions:...], the OS interprets all
-  // subsequent scroll wheel events that are part of this gesture as a swipe,
-  // and stops sending them to us.  The OS calls the trackingHandler "block"
-  // multiple times, asynchronously (sometimes after [NSEvent
-  // maybeTrackScrollEventAsSwipe:...] has returned).  The OS determines when
-  // the gesture has finished, and whether or not it was "successful" -- this
-  // information is passed to trackingHandler.  We must be careful to only
-  // call [NSEvent maybeTrackScrollEventAsSwipe:...] on a "real" swipe --
-  // otherwise two-finger scrolling performance will suffer significantly.
-  // Note that we use anEvent inside the block. This extends the lifetime of
-  // the anEvent object because it's retained by the block, see bug 682445.
-  // The block will release it when the block goes away at the end of the
-  // animation, or when the animation is canceled.
-  [anEvent trackSwipeEventWithOptions:NSEventSwipeTrackingLockDirection |
-                                      NSEventSwipeTrackingClampGestureAmount
-             dampenAmountThresholdMin:min
-                                  max:max
-                         usingHandler:^(CGFloat gestureAmount,
-                                        NSEventPhase phase,
-                                        BOOL isComplete,
-                                        BOOL *stop) {
-    uint32_t allowedDirectionsCopy = allowedDirections;
-    // Since this tracking handler can be called asynchronously, mGeckoChild
-    // might have become NULL here (our child widget might have been
-    // destroyed).
-    // Checking for gestureAmount == 0.0 also works around bug 770626, which
-    // happens when DispatchWindowEvent() triggers a modal dialog, which spins
-    // the event loop and confuses the OS. This results in several re-entrant
-    // calls to this handler.
-    if (animationCanceled || !mGeckoChild || gestureAmount == 0.0) {
-      *stop = YES;
-      animationCanceled = YES;
-      if (gestureAmount == 0.0) {
-        if (mCancelSwipeAnimation)
-          *mCancelSwipeAnimation = YES;
-        mCancelSwipeAnimation = nil;
-        [self sendSwipeEndEvent:anEvent
-              allowedDirections:allowedDirectionsCopy];
-      }
-      return;
-    }
-
-    // Update animation overlay to match gestureAmount.
-    [self sendSwipeEvent:anEvent
-                withKind:NS_SIMPLE_GESTURE_SWIPE_UPDATE
-       allowedDirections:&allowedDirectionsCopy
-               direction:0.0
-                   delta:gestureAmount];
-
-    if (phase == NSEventPhaseEnded && !geckoSwipeEventSent) {
-      // The result of the swipe is now known, so the main event can be sent.
-      // The animation might continue even after this event was sent, so
-      // don't tear down the animation overlay yet.
-
-      uint32_t directionCopy = direction;
-
-      // gestureAmount is documented to be '-1', '0' or '1' when isComplete
-      // is TRUE, but the docs don't say anything about its value at other
-      // times.  However, tests show that, when phase == NSEventPhaseEnded,
-      // gestureAmount is negative when it will be '-1' at isComplete, and
-      // positive when it will be '1'.  And phase is never equal to
-      // NSEventPhaseEnded when gestureAmount will be '0' at isComplete.
-      geckoSwipeEventSent = YES;
-      [self sendSwipeEvent:anEvent
-                  withKind:NS_SIMPLE_GESTURE_SWIPE
-         allowedDirections:&allowedDirectionsCopy
-                 direction:directionCopy
-                     delta:0.0];
-    }
-
-    if (isComplete) {
-      [self sendSwipeEndEvent:anEvent allowedDirections:allowedDirectionsCopy];
-      mCancelSwipeAnimation = nil;
-    }
-  }];
-
-  mCancelSwipeAnimation = &animationCanceled;
-}
-#endif // #ifdef __LP64__
-
 - (void)setUsingOMTCompositor:(BOOL)aUseOMTC
 {
   mUsingOMTCompositor = aUseOMTC;
 }
 
 // Returning NO from this method only disallows ordering on mousedown - in order
 // to prevent it for mouseup too, we need to call [NSApp preventWindowOrdering]
 // when handling the mousedown event.
@@ -4951,29 +4843,33 @@ IsPotentialSwipeStartEventOverscrollingV
 
   if (usePreciseDeltas && hasPhaseInformation) {
     PanGestureInput panEvent(PanGestureTypeForEvent(theEvent),
                              eventIntervalTime, eventTimeStamp,
                              position, preciseDelta, modifiers);
     panEvent.mLineOrPageDeltaX = lineOrPageDeltaX;
     panEvent.mLineOrPageDeltaY = lineOrPageDeltaY;
 
+    if (SwipeTracker* swipeTracker = mGeckoChild->GetSwipeTracker()) {
+      nsEventStatus status = swipeTracker->ProcessEvent(panEvent);
+      if (status == nsEventStatus_eConsumeNoDefault) {
+        return;
+      }
+    }
+
     widgetWheelEvent = mGeckoChild->DispatchAPZWheelInputEvent(panEvent);
 
     if (!mGeckoChild) {
       return;
     }
 
-#ifdef __LP64__
     bool canTriggerSwipe = [self shouldConsiderStartingSwipeFromEvent:theEvent];
     if (canTriggerSwipe && IsPotentialSwipeStartEventOverscrollingViewport(widgetWheelEvent)) {
-      [self maybeTrackScrollEventAsSwipe:theEvent];
+      mGeckoChild->MaybeTrackScrollEventAsSwipe(panEvent);
     }
-#endif // #ifdef __LP64__
-
   } else if (usePreciseDeltas) {
     // This is on 10.6 or old touchpads that don't have any phase information.
     ScrollWheelInput wheelEvent(eventIntervalTime, eventTimeStamp, modifiers,
                                 ScrollWheelInput::SCROLLMODE_INSTANT,
                                 ScrollWheelInput::SCROLLDELTA_PIXEL,
                                 position,
                                 preciseDelta.x,
                                 preciseDelta.y);