Bug 673875: Reproduce the bounce behavior when reaching the top/bottom of the page on OSX. r=smichaud,felipe,masayuki
☠☠ backed out by 1e935a380fb2 ☠ ☠
authorStephen Pohl <spohl.mozilla.bugs@gmail.com>
Fri, 11 Oct 2013 10:02:31 -0400
changeset 164290 751bcb37cdb6404b8d4582aad47f96af8fcd4839
parent 164289 f76526b044af992e21dc0f243c1edf974e9a5109
child 164291 05693539cfe621065d69f236252f9741eb3095b2
push id3066
push userakeybl@mozilla.com
push dateMon, 09 Dec 2013 19:58:46 +0000
treeherdermozilla-beta@a31a0dce83aa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmichaud, felipe, masayuki
bugs673875
milestone27.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 673875: Reproduce the bounce behavior when reaching the top/bottom of the page on OSX. r=smichaud,felipe,masayuki
browser/base/content/browser-gestureSupport.js
content/events/src/nsEventStateManager.cpp
widget/MouseEvents.h
widget/cocoa/nsChildView.h
widget/cocoa/nsChildView.mm
--- a/browser/base/content/browser-gestureSupport.js
+++ b/browser/base/content/browser-gestureSupport.js
@@ -57,18 +57,19 @@ let gGestureSupport = {
     }
 
     // Create a preference object with some defaults
     let def = function(aThreshold, aLatched)
       ({ threshold: aThreshold, latched: !!aLatched });
 
     switch (aEvent.type) {
       case "MozSwipeGestureStart":
-        aEvent.preventDefault();
-        this._setupSwipeGesture(aEvent);
+        if (this._setupSwipeGesture(aEvent)) {
+          aEvent.preventDefault();
+        }
         break;
       case "MozSwipeGestureUpdate":
         aEvent.preventDefault();
         this._doUpdate(aEvent);
         break;
       case "MozSwipeGestureEnd":
         aEvent.preventDefault();
         this._doEnd(aEvent);
@@ -174,44 +175,66 @@ let gGestureSupport = {
               == "Browser:ForwardOrForwardDuplicate";
   },
 
   /**
    * Sets up the history swipe animations for a swipe gesture event, if enabled.
    *
    * @param aEvent
    *        The swipe gesture start event.
+   * @return true if swipe gestures could successfully be set up, false
+   *         othwerwise.
    */
   _setupSwipeGesture: function GS__setupSwipeGesture(aEvent) {
-    if (!this._swipeNavigatesHistory(aEvent))
-      return;
+    if (!this._swipeNavigatesHistory(aEvent)) {
+      return false;
+    }
+
+    let isVerticalSwipe = false;
+    if (gHistorySwipeAnimation.active) {
+      if (aEvent.direction == aEvent.DIRECTION_UP) {
+        if (content.pageYOffset > 0) {
+          return false;
+        }
+        isVerticalSwipe = true;
+      } else if (aEvent.direction == aEvent.DIRECTION_DOWN) {
+        if (content.pageYOffset < content.scrollMaxY) {
+          return false;
+        }
+        isVerticalSwipe = true;
+      }
+    }
 
     let canGoBack = gHistorySwipeAnimation.canGoBack();
     let canGoForward = gHistorySwipeAnimation.canGoForward();
     let isLTR = gHistorySwipeAnimation.isLTR;
 
-    if (canGoBack)
+    if (canGoBack) {
       aEvent.allowedDirections |= isLTR ? aEvent.DIRECTION_LEFT :
                                           aEvent.DIRECTION_RIGHT;
-    if (canGoForward)
+    }
+    if (canGoForward) {
       aEvent.allowedDirections |= isLTR ? aEvent.DIRECTION_RIGHT :
                                           aEvent.DIRECTION_LEFT;
+    }
 
-    gHistorySwipeAnimation.startAnimation();
+    gHistorySwipeAnimation.startAnimation(isVerticalSwipe);
 
     this._doUpdate = function GS__doUpdate(aEvent) {
       gHistorySwipeAnimation.updateAnimation(aEvent.delta);
     };
 
     this._doEnd = function GS__doEnd(aEvent) {
       gHistorySwipeAnimation.swipeEndEventReceived();
 
       this._doUpdate = function (aEvent) {};
       this._doEnd = function (aEvent) {};
     }
+
+    return true;
   },
 
   /**
    * Generator producing the powerset of the input array where the first result
    * is the complete set and the last result (before StopIteration) is empty.
    *
    * @param aArray
    *        Source array containing any number of elements
@@ -547,18 +570,20 @@ let gHistorySwipeAnimation = {
 
     this.active = false;
     this.isLTR = document.documentElement.mozMatchesSelector(
                                             ":-moz-locale-dir(ltr)");
     this._trackedSnapshots = [];
     this._startingIndex = -1;
     this._historyIndex = -1;
     this._boxWidth = -1;
+    this._boxHeight = -1;
     this._maxSnapshots = this._getMaxSnapshots();
     this._lastSwipeDir = "";
+    this._direction = "horizontal";
 
     // We only want to activate history swipe animations if we store snapshots.
     // If we don't store any, we handle horizontal swipes without animations.
     if (this._maxSnapshots > 0) {
       this.active = true;
       gBrowser.addEventListener("pagehide", this, false);
       gBrowser.addEventListener("pageshow", this, false);
       gBrowser.addEventListener("popstate", this, false);
@@ -579,24 +604,38 @@ let gHistorySwipeAnimation = {
 
     this.active = false;
     this.isLTR = false;
   },
 
   /**
    * Starts the swipe animation and handles fast swiping (i.e. a swipe animation
    * is already in progress when a new one is initiated).
+   *
+   * @param aIsVerticalSwipe
+   *        Whether we're dealing with a vertical swipe or not.
    */
-  startAnimation: function HSA_startAnimation() {
+  startAnimation: function HSA_startAnimation(aIsVerticalSwipe) {
+    this._direction = aIsVerticalSwipe ? "vertical" : "horizontal";
+
     if (this.isAnimationRunning()) {
-      gBrowser.stop();
-      this._lastSwipeDir = "RELOAD"; // just ensure that != ""
-      this._canGoBack = this.canGoBack();
-      this._canGoForward = this.canGoForward();
-      this._handleFastSwiping();
+      // If this is a horizontal scroll, or if this is a vertical scroll that
+      // was started while a horizontal scroll was still running, handle it as
+      // as a fast swipe. In the case of the latter scenario, this allows us to
+      // start the vertical animation without first loading the final page, or
+      // taking another snapshot. If vertical scrolls are initiated repeatedly
+      // without prior horizontal scroll we skip this and restart the animation
+      // from 0.
+      if (this._direction == "horizontal" || this._lastSwipeDir != "") {
+        gBrowser.stop();
+        this._lastSwipeDir = "RELOAD"; // just ensure that != ""
+        this._canGoBack = this.canGoBack();
+        this._canGoForward = this.canGoForward();
+        this._handleFastSwiping();
+      }
     }
     else {
       this._startingIndex = gBrowser.webNavigation.sessionHistory.index;
       this._historyIndex = this._startingIndex;
       this._canGoBack = this.canGoBack();
       this._canGoForward = this.canGoForward();
       if (this.active) {
         this._takeSnapshot();
@@ -618,58 +657,58 @@ let gHistorySwipeAnimation = {
   /**
    * Updates the animation between two pages in history.
    *
    * @param aVal
    *        A floating point value that represents the progress of the
    *        swipe gesture.
    */
   updateAnimation: function HSA_updateAnimation(aVal) {
-    if (!this.isAnimationRunning())
+    if (!this.isAnimationRunning()) {
       return;
+    }
 
-    // We use the following value to decrease the bounce effect when swiping
-    // back/forward past the browsing history. This value was determined
-    // experimentally.
+    // We use the following value to decrease the bounce effect when scrolling
+    // to the top or bottom of the page, or when swiping back/forward past the
+    // browsing history. This value was determined experimentally.
     let dampValue = 4;
-    if ((aVal >= 0 && this.isLTR) ||
-        (aVal <= 0 && !this.isLTR)) {
+    if (this._direction == "vertical") {
+      this._prevBox.collapsed = true;
+      this._nextBox.collapsed = true;
+      this._positionBox(this._curBox, -1 * aVal / dampValue);
+    } else if ((aVal >= 0 && this.isLTR) ||
+               (aVal <= 0 && !this.isLTR)) {
       let tempDampValue = 1;
-      if (this._canGoBack)
+      if (this._canGoBack) {
         this._prevBox.collapsed = false;
-      else {
+      } else {
         tempDampValue = dampValue;
         this._prevBox.collapsed = true;
       }
 
       // The current page is pushed to the right (LTR) or left (RTL),
       // the intention is to go back.
       // If there is a page to go back to, it should show in the background.
       this._positionBox(this._curBox, aVal / tempDampValue);
 
       // The forward page should be pushed offscreen all the way to the right.
       this._positionBox(this._nextBox, 1);
-    }
-    else {
-      if (aVal < -1)
-        aVal = -1; // Cap value to avoid sliding the page further than allowed.
-
+    } else {
       // The intention is to go forward. If there is a page to go forward to,
       // it should slide in from the right (LTR) or left (RTL).
       // Otherwise, the current page should slide to the left (LTR) or
       // right (RTL) and the backdrop should appear in the background.
       // For the backdrop to be visible in that case, the previous page needs
       // to be hidden (if it exists).
       if (this._canGoForward) {
         this._nextBox.collapsed = false;
         let offset = this.isLTR ? 1 : -1;
         this._positionBox(this._curBox, 0);
         this._positionBox(this._nextBox, offset + aVal);
-      }
-      else {
+      } else {
         this._prevBox.collapsed = true;
         this._positionBox(this._curBox, aVal / dampValue);
       }
     }
   },
 
   /**
    * Event handler for events relevant to the history swipe animation.
@@ -830,30 +869,33 @@ let gHistorySwipeAnimation = {
     this._curBox = this._createElement("historySwipeAnimationCurrentPage",
                                        "box");
     this._container.appendChild(this._curBox);
 
     this._nextBox = this._createElement("historySwipeAnimationNextPage",
                                         "box");
     this._container.appendChild(this._nextBox);
 
-    this._boxWidth = this._curBox.getBoundingClientRect().width; // cache width
+    // Cache width and height.
+    this._boxWidth = this._curBox.getBoundingClientRect().width;
+    this._boxHeight = this._curBox.getBoundingClientRect().height;
   },
 
   /**
    * Removes the boxes.
    */
   _removeBoxes: function HSA__removeBoxes() {
     this._curBox = null;
     this._prevBox = null;
     this._nextBox = null;
     if (this._container)
       this._container.parentNode.removeChild(this._container);
     this._container = null;
     this._boxWidth = -1;
+    this._boxHeight = -1;
   },
 
   /**
    * Creates an element with a given identifier and tag name.
    *
    * @param aID
    *        An identifier to create the element with.
    * @param aTagName
@@ -871,17 +913,24 @@ let gHistorySwipeAnimation = {
    * Moves a given box to a given X coordinate position.
    *
    * @param aBox
    *        The box element to position.
    * @param aPosition
    *        The position (in X coordinates) to move the box element to.
    */
   _positionBox: function HSA__positionBox(aBox, aPosition) {
-    aBox.style.transform = "translateX(" + this._boxWidth * aPosition + "px)";
+    let transform = "";
+
+    if (this._direction == "vertical")
+      transform = "translateY(" + this._boxHeight * aPosition + "px)";
+    else
+      transform = "translateX(" + this._boxWidth * aPosition + "px)";
+
+    aBox.style.transform = transform;
   },
 
   /**
    * Takes a snapshot of the page the browser is currently on.
    */
   _takeSnapshot: function HSA__takeSnapshot() {
     if ((this._maxSnapshots < 1) ||
         (gBrowser.webNavigation.sessionHistory.index < 0))
--- a/content/events/src/nsEventStateManager.cpp
+++ b/content/events/src/nsEventStateManager.cpp
@@ -3184,16 +3184,19 @@ nsEventStateManager::PostHandleEvent(nsP
           if (!wheelEvent->deltaX && !wheelEvent->deltaY) {
             break;
           }
           // For scrolling of default action, we should honor the mouse wheel
           // transaction.
           nsIScrollableFrame* scrollTarget =
             ComputeScrollTarget(aTargetFrame, wheelEvent,
                                 COMPUTE_DEFAULT_ACTION_TARGET);
+          if (!scrollTarget) {
+            wheelEvent->mViewPortIsOverscrolled = true;
+          }
           wheelEvent->overflowDeltaX = wheelEvent->deltaX;
           wheelEvent->overflowDeltaY = wheelEvent->deltaY;
           WheelPrefs::GetInstance()->
             CancelApplyingUserPrefsFromOverflowDelta(wheelEvent);
           if (scrollTarget) {
             DoScrollText(scrollTarget, wheelEvent);
           } else {
             nsMouseWheelTransaction::EndTransaction();
--- a/widget/MouseEvents.h
+++ b/widget/MouseEvents.h
@@ -313,17 +313,18 @@ private:
 
 public:
   WidgetWheelEvent(bool aIsTrusted, uint32_t aMessage, nsIWidget* aWidget) :
     WidgetMouseEventBase(aIsTrusted, aMessage, aWidget, NS_WHEEL_EVENT),
     deltaX(0.0), deltaY(0.0), deltaZ(0.0),
     deltaMode(nsIDOMWheelEvent::DOM_DELTA_PIXEL),
     customizedByUserPrefs(false), isMomentum(false), isPixelOnlyDevice(false),
     lineOrPageDeltaX(0), lineOrPageDeltaY(0), scrollType(SCROLL_DEFAULT),
-    overflowDeltaX(0.0), overflowDeltaY(0.0)
+    overflowDeltaX(0.0), overflowDeltaY(0.0),
+    mViewPortIsOverscrolled(false)
   {
   }
 
   // NOTE: deltaX, deltaY and deltaZ may be customized by
   //       mousewheel.*.delta_multiplier_* prefs which are applied by
   //       nsEventStateManager.  So, after widget dispatches this event,
   //       these delta values may have different values than before.
   double deltaX;
@@ -394,32 +395,39 @@ public:
   // NOTE: deltaX, deltaY and deltaZ may be modified by nsEventStateManager.
   //       However, overflowDeltaX and overflowDeltaY indicate unused original
   //       delta values which are not applied the delta_multiplier prefs.
   //       So, if widget wanted to know the actual direction to be scrolled,
   //       it would need to check the deltaX and deltaY.
   double overflowDeltaX;
   double overflowDeltaY;
 
+  // Whether or not the parent of the currently overscrolled frame is the
+  // ViewPort. This is false in situations when an element on the page is being
+  // overscrolled (such as a text field), but true when the 'page' is being
+  // overscrolled.
+  bool mViewPortIsOverscrolled;
+
   void AssignWheelEventData(const WidgetWheelEvent& aEvent, bool aCopyTargets)
   {
     AssignMouseEventBaseData(aEvent, aCopyTargets);
 
     deltaX = aEvent.deltaX;
     deltaY = aEvent.deltaY;
     deltaZ = aEvent.deltaZ;
     deltaMode = aEvent.deltaMode;
     customizedByUserPrefs = aEvent.customizedByUserPrefs;
     isMomentum = aEvent.isMomentum;
     isPixelOnlyDevice = aEvent.isPixelOnlyDevice;
     lineOrPageDeltaX = aEvent.lineOrPageDeltaX;
     lineOrPageDeltaY = aEvent.lineOrPageDeltaY;
     scrollType = aEvent.scrollType;
     overflowDeltaX = aEvent.overflowDeltaX;
     overflowDeltaY = aEvent.overflowDeltaY;
+    mViewPortIsOverscrolled = aEvent.mViewPortIsOverscrolled;
   }
 };
 
 // TODO: Remove following typedef
 typedef WidgetWheelEvent                WheelEvent;
 
 } // namespace mozilla
 
--- a/widget/cocoa/nsChildView.h
+++ b/widget/cocoa/nsChildView.h
@@ -285,16 +285,17 @@ typedef NSInteger NSEventGestureAxis;
   float mCumulativeRotation;
 
   BOOL mDidForceRefreshOpenGL;
   BOOL mWaitingForPaint;
 
 #ifdef __LP64__
   // Support for fluid swipe tracking.
   BOOL* mCancelSwipeAnimation;
+  uint32_t mCurrentSwipeDir;
 #endif
 
   // Whether this uses off-main-thread compositing.
   BOOL mUsingOMTCompositor;
 
   // The mask image that's used when painting into the titlebar using basic
   // CGContext painting (i.e. non-accelerated).
   CGImageRef mTopLeftCornerMask;
@@ -355,17 +356,19 @@ typedef NSInteger NSEventGestureAxis;
 - (void)endGestureWithEvent:(NSEvent *)anEvent;
 
 // Helper function for Lion smart magnify events
 + (BOOL)isLionSmartMagnifyEvent:(NSEvent*)anEvent;
 
 // Support for fluid swipe tracking.
 #ifdef __LP64__
 - (void)maybeTrackScrollEventAsSwipe:(NSEvent *)anEvent
-                      scrollOverflow:(double)overflow;
+                     scrollOverflowX:(double)anOverflowX
+                     scrollOverflowY:(double)anOverflowY
+              viewPortIsOverscrolled:(BOOL)aViewPortIsOverscrolled;
 #endif
 
 - (void)setUsingOMTCompositor:(BOOL)aUseOMTC;
 @end
 
 class ChildViewMouseTracker {
 
 public:
--- a/widget/cocoa/nsChildView.mm
+++ b/widget/cocoa/nsChildView.mm
@@ -2791,16 +2791,17 @@ NSEvent* gLastDragMouseDownEvent = nil;
     // we call it in drawRect:inContext:, when we know that a draw is in
     // progress.
     mDidForceRefreshOpenGL = NO;
 
     [self setFocusRingType:NSFocusRingTypeNone];
 
 #ifdef __LP64__
     mCancelSwipeAnimation = nil;
+    mCurrentSwipeDir = 0;
 #endif
 
     mTopLeftCornerMask = NULL;
   }
 
   // register for things we'll take from other applications
   [ChildView registerViewForDraggedTypes:self];
 
@@ -4088,91 +4089,125 @@ NSEvent* gLastDragMouseDownEvent = nil;
     uint32_t allowedDirectionsCopy = aAllowedDirections;
     [self sendSwipeEvent:anEvent
                 withKind:NS_SIMPLE_GESTURE_SWIPE_END
        allowedDirections:&allowedDirectionsCopy
                direction:0
                    delta:0.0];
 }
 
-// Support fluid swipe tracking on OS X 10.7 and higher.  We must be careful
-// to only invoke this support on a horizontal two-finger gesture that really
+// 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
+// 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 swipe has the same functionality as the Back and Forward buttons.  For
-// now swipe animation is unsupported (e.g. no bounces).  This method is
-// partly based on Apple sample code available at
-// http://developer.apple.com/library/mac/#releasenotes/Cocoa/AppKit.html
+// 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
-                      scrollOverflow:(double)overflow
+                     scrollOverflowX:(double)anOverflowX
+                     scrollOverflowY:(double)anOverflowY
+              viewPortIsOverscrolled:(BOOL)aViewPortIsOverscrolled
 {
   if (!nsCocoaFeatures::OnLionOrLater()) {
     return;
   }
 
   // This method checks whether the AppleEnableSwipeNavigateWithScrolls global
   // preference is set.  If it isn't, fluid swipe tracking is disabled, and a
   // horizontal two-finger gesture is always a scroll (even in Safari).  This
   // preference can't (currently) be set from the Preferences UI -- only using
   // 'defaults write'.
   if (![NSEvent isSwipeTrackingFromScrollEventsEnabled]) {
     return;
   }
 
+  // We should only track scroll events as swipe if the viewport is being
+  // overscrolled.
+  if (!aViewPortIsOverscrolled) {
+    return;
+  }
+
   // Verify that this is a scroll wheel event with proper phase to be tracked
   // by the OS.
   if ([anEvent type] != NSScrollWheel || [anEvent phase] == NSEventPhaseNone) {
     return;
   }
 
   // Only initiate tracking if the user has tried to scroll past the edge of
-  // the current page (as indicated by 'overflow' being non-zero).  Gecko only
-  // sets WidgetMouseScrollEvent.scrollOverflow when it's processing
-  // NS_MOUSE_PIXEL_SCROLL events (not NS_MOUSE_SCROLL events).
-  // WidgetMouseScrollEvent.scrollOverflow only indicates left or right overflow
-  // for horizontal NS_MOUSE_PIXEL_SCROLL events.
-  if (!overflow) {
+  // the current page (as indicated by 'anOverflowX' or 'anOverflowY' being
+  // non-zero). Gecko only sets WidgetMouseScrollEvent.scrollOverflow when it's
+  // processing NS_MOUSE_PIXEL_SCROLL events (not NS_MOUSE_SCROLL events).
+  if (anOverflowX == 0.0 && anOverflowY == 0.0) {
     return;
   }
 
   CGFloat deltaX, deltaY;
   if ([anEvent hasPreciseScrollingDeltas]) {
     deltaX = [anEvent scrollingDeltaX];
     deltaY = [anEvent scrollingDeltaY];
   } else {
-    deltaX = [anEvent deltaX];
-    deltaY = [anEvent deltaY];
-  }
-
+    return;
+  }
+
+  uint32_t vDirs = (uint32_t)nsIDOMSimpleGestureEvent::DIRECTION_DOWN |
+                   (uint32_t)nsIDOMSimpleGestureEvent::DIRECTION_UP;
   uint32_t direction = 0;
+
   // 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.
-  if (overflow != 0.0 && deltaX != 0.0 &&
+  if (anOverflowX != 0.0 && deltaX != 0.0 &&
       fabsf(deltaX) > fabsf(deltaY) * 8) {
     // Only initiate horizontal tracking for gestures that have just begun --
     // otherwise a scroll to one side of the page can have a swipe tacked on
     // to it.
     if ([anEvent phase] != NSEventPhaseBegan) {
       return;
     }
 
     if (deltaX < 0.0) {
       direction = (uint32_t)nsIDOMSimpleGestureEvent::DIRECTION_RIGHT;
     } else {
       direction = (uint32_t)nsIDOMSimpleGestureEvent::DIRECTION_LEFT;
     }
+  }
+  // Only initiate vertical tracking for events whose vertical element is
+  // at least two times larger than its horizontal element. This minimizes
+  // performance problems. The number '2' was arrived at by trial and error.
+  else if (anOverflowY != 0.0 && deltaY != 0.0 &&
+           fabsf(deltaY) > fabsf(deltaX) * 2) {
+    if (deltaY < 0.0) {
+      direction = (uint32_t)nsIDOMSimpleGestureEvent::DIRECTION_DOWN;
+    } else {
+      direction = (uint32_t)nsIDOMSimpleGestureEvent::DIRECTION_UP;
+    }
+
+    if ((mCurrentSwipeDir & vDirs) && (mCurrentSwipeDir != direction)) {
+      // 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;
+        [self sendSwipeEndEvent:anEvent allowedDirections:0];
+      }
+      return;
+    }
   } else {
     return;
   }
 
+  // Track the direction we're going in.
+  mCurrentSwipeDir = direction;
+
   // 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;
   }
 
   uint32_t allowedDirections = 0;
@@ -4184,18 +4219,24 @@ NSEvent* gLastDragMouseDownEvent = nil;
                              allowedDirections:&allowedDirections
                                      direction:direction
                                          delta:0.0];
 
   if (!shouldStartSwipe) {
     return;
   }
 
-  double min = (allowedDirections & nsIDOMSimpleGestureEvent::DIRECTION_RIGHT) ? -1 : 0;
-  double max = (allowedDirections & nsIDOMSimpleGestureEvent::DIRECTION_LEFT) ? 1 : 0;
+  CGFloat min = 0.0;
+  CGFloat max = 0.0;
+  if (!(direction & vDirs)) {
+    min = (allowedDirections & nsIDOMSimpleGestureEvent::DIRECTION_RIGHT) ?
+          -1.0 : 0.0;
+    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"
@@ -4223,23 +4264,25 @@ NSEvent* gLastDragMouseDownEvent = nil;
     // 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 (gestureAmount == 0.0 ||
+          ((direction & vDirs) && (direction != mCurrentSwipeDir))) {
         if (mCancelSwipeAnimation)
           *mCancelSwipeAnimation = YES;
         mCancelSwipeAnimation = nil;
         [self sendSwipeEndEvent:anEvent
               allowedDirections:allowedDirectionsCopy];
       }
+      mCurrentSwipeDir = 0;
       return;
     }
 
     // Update animation overlay to match gestureAmount.
     [self sendSwipeEvent:anEvent
                 withKind:NS_SIMPLE_GESTURE_SWIPE_UPDATE
        allowedDirections:&allowedDirectionsCopy
                direction:0.0
@@ -4263,16 +4306,17 @@ NSEvent* gLastDragMouseDownEvent = nil;
                   withKind:NS_SIMPLE_GESTURE_SWIPE
          allowedDirections:&allowedDirectionsCopy
                  direction:directionCopy
                      delta:0.0];
     }
 
     if (isComplete) {
       [self sendSwipeEndEvent:anEvent allowedDirections:allowedDirectionsCopy];
+      mCurrentSwipeDir = 0;
       mCancelSwipeAnimation = nil;
     }
   }];
 
   mCancelSwipeAnimation = &animationCanceled;
 }
 #endif // #ifdef __LP64__
 
@@ -4775,28 +4819,39 @@ static int32_t RoundUp(double aDouble)
 
   wheelEvent.isMomentum = nsCocoaUtils::IsMomentumScrollEvent(theEvent);
 
   NPCocoaEvent cocoaEvent;
   ChildViewMouseTracker::AttachPluginEvent(wheelEvent, self, theEvent,
                                            NPCocoaEventScrollWheel,
                                            &cocoaEvent);
 
-  mGeckoChild->DispatchWindowEvent(wheelEvent);
-  if (!mGeckoChild) {
-    return;
+  // Only dispatch this event if we're not currently tracking a scroll event as
+  // swipe.
+  if (!mCancelSwipeAnimation || *mCancelSwipeAnimation == YES) {
+    mGeckoChild->DispatchWindowEvent(wheelEvent);
+    if (!mGeckoChild) {
+      return;
+    }
+  } else {
+    // Manually set these members here since we didn't dispatch the event.
+    wheelEvent.overflowDeltaX = wheelEvent.deltaX;
+    wheelEvent.overflowDeltaY = wheelEvent.deltaY;
+    wheelEvent.mViewPortIsOverscrolled = true;
   }
 
 #ifdef __LP64__
-  // overflowDeltaX tells us when the user has tried to scroll past the edge
-  // of a page to the left or the right (in those cases it's non-zero).
-  if (wheelEvent.deltaMode == nsIDOMWheelEvent::DOM_DELTA_PIXEL &&
-      wheelEvent.deltaX != 0.0) {
+  // overflowDeltaX and overflowDeltaY tell us when the user has tried to
+  // scroll past the edge of a page (in those cases it's non-zero).
+  if ((wheelEvent.deltaMode == nsIDOMWheelEvent::DOM_DELTA_PIXEL) &&
+      (wheelEvent.deltaX != 0.0 || wheelEvent.deltaY != 0.0)) {
     [self maybeTrackScrollEventAsSwipe:theEvent
-                        scrollOverflow:wheelEvent.overflowDeltaX];
+                       scrollOverflowX:wheelEvent.overflowDeltaX
+                       scrollOverflowY:wheelEvent.overflowDeltaY
+                viewPortIsOverscrolled:wheelEvent.mViewPortIsOverscrolled];
   }
 #endif // #ifdef __LP64__
 
   NS_OBJC_END_TRY_ABORT_BLOCK;
 }
 
 -(NSMenu*)menuForEvent:(NSEvent*)theEvent
 {