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 150489 751bcb37cdb6404b8d4582aad47f96af8fcd4839
parent 150488 f76526b044af992e21dc0f243c1edf974e9a5109
child 150490 05693539cfe621065d69f236252f9741eb3095b2
push id3001
push userryanvm@gmail.com
push dateFri, 11 Oct 2013 21:03:10 +0000
treeherderfx-team@9ab188de8245 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmichaud, felipe, masayuki
bugs673875
milestone27.0a1
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
 {