Bug 945584: Part 6 - Implementation of scroll snapping (v10 Patch), r=roc, r=kats
authorKearwood (Kip) Gilbert <kgilbert@mozilla.com>
Thu, 19 Feb 2015 15:53:30 -0800
changeset 232126 b96d3d51d4b4ef17c35ac3429a455730e0bd2854
parent 232125 a325a7b90b913da42f531a093784e60b8c812c9f
child 232127 79989474e408ef46f51ed515a2202310b54b8b4c
push id56458
push userkwierso@gmail.com
push dateFri, 06 Mar 2015 00:23:01 +0000
treeherdermozilla-inbound@1e3ba87c7800 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersroc, kats
bugs945584
milestone39.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 945584: Part 6 - Implementation of scroll snapping (v10 Patch), r=roc, r=kats - Implemented CSS scroll snapping (http://dev.w3.org/csswg/css-snappoints/)
dom/events/EventStateManager.cpp
dom/ipc/PBrowser.ipdl
dom/ipc/TabChild.cpp
dom/ipc/TabChild.h
dom/ipc/TabParent.cpp
dom/ipc/TabParent.h
gfx/layers/apz/public/GeckoContentController.h
gfx/layers/apz/src/AsyncPanZoomController.cpp
gfx/layers/apz/util/APZCCallbackHelper.cpp
gfx/layers/apz/util/APZCCallbackHelper.h
gfx/layers/apz/util/ChromeProcessController.cpp
gfx/layers/apz/util/ChromeProcessController.h
gfx/tests/gtest/TestAsyncPanZoomController.cpp
layout/base/nsPresShell.cpp
layout/generic/nsGfxScrollFrame.cpp
layout/generic/nsGfxScrollFrame.h
layout/generic/nsIScrollableFrame.h
layout/ipc/RenderFrameParent.cpp
widget/android/APZCCallbackHandler.cpp
widget/android/APZCCallbackHandler.h
widget/windows/winrt/APZController.cpp
widget/windows/winrt/APZController.h
--- a/dom/events/EventStateManager.cpp
+++ b/dom/events/EventStateManager.cpp
@@ -2412,23 +2412,26 @@ EventStateManager::DoScrollText(nsIScrol
   ScrollbarStyles overflowStyle = aScrollableFrame->GetScrollbarStyles();
   if (overflowStyle.mHorizontal == NS_STYLE_OVERFLOW_HIDDEN) {
     actualDevPixelScrollAmount.x = 0;
   }
   if (overflowStyle.mVertical == NS_STYLE_OVERFLOW_HIDDEN) {
     actualDevPixelScrollAmount.y = 0;
   }
 
+  nsIScrollableFrame::ScrollSnapMode snapMode = nsIScrollableFrame::DISABLE_SNAP;
   nsIAtom* origin = nullptr;
   switch (aEvent->deltaMode) {
     case nsIDOMWheelEvent::DOM_DELTA_LINE:
       origin = nsGkAtoms::mouseWheel;
+      snapMode = nsIScrollableFrame::ENABLE_SNAP;
       break;
     case nsIDOMWheelEvent::DOM_DELTA_PAGE:
       origin = nsGkAtoms::pages;
+      snapMode = nsIScrollableFrame::ENABLE_SNAP;
       break;
     case nsIDOMWheelEvent::DOM_DELTA_PIXEL:
       origin = nsGkAtoms::pixels;
       break;
     default:
       MOZ_CRASH("Invalid deltaMode value comes");
   }
 
@@ -2478,17 +2481,17 @@ EventStateManager::DoScrollText(nsIScrol
 
   nsIScrollableFrame::ScrollMomentum momentum =
     aEvent->isMomentum ? nsIScrollableFrame::SYNTHESIZED_MOMENTUM_EVENT
                        : nsIScrollableFrame::NOT_MOMENTUM;
 
   nsIntPoint overflow;
   aScrollableFrame->ScrollBy(actualDevPixelScrollAmount,
                              nsIScrollableFrame::DEVICE_PIXELS,
-                             mode, &overflow, origin, momentum);
+                             mode, &overflow, origin, momentum, snapMode);
 
   if (!scrollFrameWeak.IsAlive()) {
     // If the scroll causes changing the layout, we can think that the event
     // has been completely consumed by the content.  Then, users probably don't
     // want additional action.
     aEvent->overflowDeltaX = aEvent->overflowDeltaY = 0;
   } else if (isDeltaModePixel) {
     aEvent->overflowDeltaX = overflow.x;
@@ -2979,16 +2982,23 @@ EventStateManager::PostHandleEvent(nsPre
         frameSelection->SetDragState(false);
       }
     }
     break;
   case NS_WHEEL_STOP:
     {
       MOZ_ASSERT(aEvent->mFlags.mIsTrusted);
       ScrollbarsForWheel::MayInactivate();
+      WidgetWheelEvent* wheelEvent = aEvent->AsWheelEvent();
+      nsIScrollableFrame* scrollTarget =
+        ComputeScrollTarget(aTargetFrame, wheelEvent,
+                            COMPUTE_DEFAULT_ACTION_TARGET);
+      if (scrollTarget) {
+        scrollTarget->ScrollSnap();
+      }
     }
     break;
   case NS_WHEEL_WHEEL:
   case NS_WHEEL_START:
     {
       MOZ_ASSERT(aEvent->mFlags.mIsTrusted);
 
       if (*aStatus == nsEventStatus_eConsumeNoDefault) {
--- a/dom/ipc/PBrowser.ipdl
+++ b/dom/ipc/PBrowser.ipdl
@@ -508,16 +508,17 @@ child:
     UpdateDimensions(nsIntRect rect, ScreenIntSize size, ScreenOrientation orientation,
                      nsIntPoint chromeDisp) compress;
 
     UpdateFrame(FrameMetrics frame);
 
     // The following methods correspond to functions on the GeckoContentController
     // interface in gfx/layers/apz/public/GeckoContentController.h. Refer to documentation
     // in that file for these functions.
+    RequestFlingSnap(ViewID aScrollID, CSSPoint aDestination);
     AcknowledgeScrollUpdate(ViewID aScrollId, uint32_t aScrollGeneration);
     HandleDoubleTap(CSSPoint aPoint, Modifiers aModifiers, ScrollableLayerGuid aGuid);
     HandleSingleTap(CSSPoint aPoint, Modifiers aModifiers, ScrollableLayerGuid aGuid);
     HandleLongTap(CSSPoint point, Modifiers aModifiers, ScrollableLayerGuid aGuid, uint64_t aInputBlockId);
     HandleLongTapUp(CSSPoint point, Modifiers aModifiers, ScrollableLayerGuid aGuid);
     NotifyAPZStateChange(ViewID aViewId, APZStateChange aChange, int aArg);
 
 
--- a/dom/ipc/TabChild.cpp
+++ b/dom/ipc/TabChild.cpp
@@ -2032,16 +2032,24 @@ TabChild::RecvUpdateDimensions(const nsI
 
 bool
 TabChild::RecvUpdateFrame(const FrameMetrics& aFrameMetrics)
 {
   return TabChildBase::UpdateFrameHandler(aFrameMetrics);
 }
 
 bool
+TabChild::RecvRequestFlingSnap(const FrameMetrics::ViewID& aScrollId,
+                               const mozilla::CSSPoint& aDestination)
+{
+  APZCCallbackHelper::RequestFlingSnap(aScrollId, aDestination);
+  return true;
+}
+
+bool
 TabChild::RecvAcknowledgeScrollUpdate(const ViewID& aScrollId,
                                       const uint32_t& aScrollGeneration)
 {
   APZCCallbackHelper::AcknowledgeScrollUpdate(aScrollId, aScrollGeneration);
   return true;
 }
 
 bool
--- a/dom/ipc/TabChild.h
+++ b/dom/ipc/TabChild.h
@@ -314,16 +314,18 @@ public:
                           const uint64_t& aLayersId,
                           PRenderFrameChild* aRenderFrame,
                           const bool& aParentIsActive) MOZ_OVERRIDE;
     virtual bool RecvUpdateDimensions(const nsIntRect& rect,
                                       const ScreenIntSize& size,
                                       const ScreenOrientation& orientation,
                                       const nsIntPoint& chromeDisp) MOZ_OVERRIDE;
     virtual bool RecvUpdateFrame(const layers::FrameMetrics& aFrameMetrics) MOZ_OVERRIDE;
+    virtual bool RecvRequestFlingSnap(const ViewID& aScrollId,
+                                      const CSSPoint& aDestination) MOZ_OVERRIDE;
     virtual bool RecvAcknowledgeScrollUpdate(const ViewID& aScrollId,
                                              const uint32_t& aScrollGeneration) MOZ_OVERRIDE;
     virtual bool RecvHandleDoubleTap(const CSSPoint& aPoint,
                                      const Modifiers& aModifiers,
                                      const mozilla::layers::ScrollableLayerGuid& aGuid) MOZ_OVERRIDE;
     virtual bool RecvHandleSingleTap(const CSSPoint& aPoint,
                                      const Modifiers& aModifiers,
                                      const mozilla::layers::ScrollableLayerGuid& aGuid) MOZ_OVERRIDE;
--- a/dom/ipc/TabParent.cpp
+++ b/dom/ipc/TabParent.cpp
@@ -918,16 +918,25 @@ TabParent::UIResolutionChanged()
     // TryCacheDPIAndScale()'s cache is keyed off of
     // mDPI being greater than 0, so this invalidates it.
     mDPI = -1;
     unused << SendUIResolutionChanged();
   }
 }
 
 void
+TabParent::RequestFlingSnap(const FrameMetrics::ViewID& aScrollId,
+                            const mozilla::CSSPoint& aDestination)
+{
+  if (!mIsDestroyed) {
+    unused << SendRequestFlingSnap(aScrollId, aDestination);
+  }
+}
+
+void
 TabParent::AcknowledgeScrollUpdate(const ViewID& aScrollId, const uint32_t& aScrollGeneration)
 {
   if (!mIsDestroyed) {
     unused << SendAcknowledgeScrollUpdate(aScrollId, aScrollGeneration);
   }
 }
 
 void TabParent::HandleDoubleTap(const CSSPoint& aPoint,
--- a/dom/ipc/TabParent.h
+++ b/dom/ipc/TabParent.h
@@ -240,16 +240,18 @@ public:
     // XXX/cjones: it's not clear what we gain by hiding these
     // message-sending functions under a layer of indirection and
     // eating the return values
     void Show(const ScreenIntSize& size, bool aParentIsActive);
     void UpdateDimensions(const nsIntRect& rect, const ScreenIntSize& size,
                           const nsIntPoint& chromeDisp);
     void UpdateFrame(const layers::FrameMetrics& aFrameMetrics);
     void UIResolutionChanged();
+    void RequestFlingSnap(const FrameMetrics::ViewID& aScrollId,
+                          const mozilla::CSSPoint& aDestination);
     void AcknowledgeScrollUpdate(const ViewID& aScrollId, const uint32_t& aScrollGeneration);
     void HandleDoubleTap(const CSSPoint& aPoint,
                          Modifiers aModifiers,
                          const ScrollableLayerGuid& aGuid);
     void HandleSingleTap(const CSSPoint& aPoint,
                          Modifiers aModifiers,
                          const ScrollableLayerGuid& aGuid);
     void HandleLongTap(const CSSPoint& aPoint,
--- a/gfx/layers/apz/public/GeckoContentController.h
+++ b/gfx/layers/apz/public/GeckoContentController.h
@@ -26,16 +26,24 @@ public:
   /**
    * Requests a paint of the given FrameMetrics |aFrameMetrics| from Gecko.
    * Implementations per-platform are responsible for actually handling this.
    * This method will always be called on the Gecko main thread.
    */
   virtual void RequestContentRepaint(const FrameMetrics& aFrameMetrics) = 0;
 
   /**
+   * Requests handling of a scroll snapping at the end of a fling gesture for
+   * the scrollable frame with the given scroll id. aDestination specifies the
+   * expected landing position of the fling if no snapping were to be performed.
+   */
+  virtual void RequestFlingSnap(const FrameMetrics::ViewID& aScrollId,
+                                const mozilla::CSSPoint& aDestination) = 0;
+
+  /**
    * Acknowledges the recipt of a scroll offset update for the scrollable
    * frame with the given scroll id. This is used to maintain consistency
    * between APZ and other sources of scroll changes.
    */
   virtual void AcknowledgeScrollUpdate(const FrameMetrics::ViewID& aScrollId,
                                        const uint32_t& aScrollGeneration) = 0;
 
   /**
--- a/gfx/layers/apz/src/AsyncPanZoomController.cpp
+++ b/gfx/layers/apz/src/AsyncPanZoomController.cpp
@@ -1975,19 +1975,47 @@ nsRefPtr<const OverscrollHandoffChain> A
 void AsyncPanZoomController::AcceptFling(const ParentLayerPoint& aVelocity,
                                          const nsRefPtr<const OverscrollHandoffChain>& aOverscrollHandoffChain,
                                          bool aHandoff) {
   // We may have a pre-existing velocity for whatever reason (for example,
   // a previously handed off fling). We don't want to clobber that.
   mX.SetVelocity(mX.GetVelocity() + aVelocity.x);
   mY.SetVelocity(mY.GetVelocity() + aVelocity.y);
   SetState(FLING);
-  StartAnimation(new FlingAnimation(*this,
+  FlingAnimation *fling = new FlingAnimation(*this,
       aOverscrollHandoffChain,
-      !aHandoff));  // only apply acceleration if this is an initial fling
+      !aHandoff);  // only apply acceleration if this is an initial fling
+
+  float friction = gfxPrefs::APZFlingFriction();
+  ParentLayerPoint velocity(mX.GetVelocity(), mY.GetVelocity());
+  ParentLayerPoint predictedDelta;
+  // "-velocity / log(1.0 - friction)" is the integral of the deceleration
+  // curve modeled for flings in the "Axis" class.
+  if (velocity.x != 0.0f) {
+    predictedDelta.x = -velocity.x / log(1.0 - friction);
+  }
+  if (velocity.y != 0.0f) {
+    predictedDelta.y = -velocity.y / log(1.0 - friction);
+  }
+  CSSPoint predictedDestination = mFrameMetrics.GetScrollOffset() + predictedDelta / mFrameMetrics.GetZoom();
+
+  nsRefPtr<GeckoContentController> controller = GetGeckoContentController();
+  if (controller) {
+    APZC_LOG("%p fling snapping.  friction: %f velocity: %f, %f "
+             "predictedDelta: %f, %f position: %f, %f "
+             "predictedDestination: %f, %f\n",
+             this, friction, velocity.x, velocity.y, (float)predictedDelta.x,
+             (float)predictedDelta.y, (float)mFrameMetrics.GetScrollOffset().x,
+             (float)mFrameMetrics.GetScrollOffset().y,
+             (float)predictedDestination.x, (float)predictedDestination.y);
+    controller->RequestFlingSnap(mFrameMetrics.GetScrollId(),
+                                 predictedDestination);
+  }
+
+  StartAnimation(fling);
 }
 
 bool AsyncPanZoomController::AttemptFling(ParentLayerPoint aVelocity,
                                           const nsRefPtr<const OverscrollHandoffChain>& aOverscrollHandoffChain,
                                           bool aHandoff) {
   // If we are pannable, take over the fling ourselves.
   if (IsPannable()) {
     AcceptFling(aVelocity,
--- a/gfx/layers/apz/util/APZCCallbackHelper.cpp
+++ b/gfx/layers/apz/util/APZCCallbackHelper.cpp
@@ -259,16 +259,56 @@ APZCCallbackHelper::GetOrCreateScrollIde
     if (!aContent) {
         return false;
     }
     *aViewIdOut = nsLayoutUtils::FindOrCreateIDFor(aContent);
     nsCOMPtr<nsIDOMWindowUtils> utils = GetDOMWindowUtils(aContent);
     return utils && (utils->GetPresShellId(aPresShellIdOut) == NS_OK);
 }
 
+class FlingSnapEvent : public nsRunnable
+{
+    typedef mozilla::layers::FrameMetrics::ViewID ViewID;
+
+public:
+    FlingSnapEvent(const ViewID& aScrollId,
+                   const mozilla::CSSPoint& aDestination)
+        : mScrollId(aScrollId)
+        , mDestination(aDestination)
+    {
+    }
+
+    NS_IMETHOD Run() {
+        MOZ_ASSERT(NS_IsMainThread());
+
+        nsIScrollableFrame* sf = nsLayoutUtils::FindScrollableFrameFor(mScrollId);
+        if (sf) {
+            sf->FlingSnap(mDestination);
+        }
+
+        return NS_OK;
+    }
+
+protected:
+    ViewID mScrollId;
+    mozilla::CSSPoint mDestination;
+};
+
+void
+APZCCallbackHelper::RequestFlingSnap(const FrameMetrics::ViewID& aScrollId,
+                                     const mozilla::CSSPoint& aDestination)
+{
+    nsCOMPtr<nsIRunnable> r1 = new FlingSnapEvent(aScrollId, aDestination);
+    if (!NS_IsMainThread()) {
+        NS_DispatchToMainThread(r1);
+    } else {
+        r1->Run();
+    }
+}
+
 class AcknowledgeScrollUpdateEvent : public nsRunnable
 {
     typedef mozilla::layers::FrameMetrics::ViewID ViewID;
 
 public:
     AcknowledgeScrollUpdateEvent(const ViewID& aScrollId, const uint32_t& aScrollGeneration)
         : mScrollId(aScrollId)
         , mScrollGeneration(aScrollGeneration)
--- a/gfx/layers/apz/util/APZCCallbackHelper.h
+++ b/gfx/layers/apz/util/APZCCallbackHelper.h
@@ -76,16 +76,24 @@ public:
     /* Get the presShellId and view ID for the given content element.
      * If the view ID does not exist, one is created.
      * The pres shell ID should generally already exist; if it doesn't for some
      * reason, false is returned. */
     static bool GetOrCreateScrollIdentifiers(nsIContent* aContent,
                                              uint32_t* aPresShellIdOut,
                                              FrameMetrics::ViewID* aViewIdOut);
 
+    /* Tell layout to perform scroll snapping for the scrollable frame with the
+     * given scroll id. aDestination specifies the expected landing position of
+     * a current fling or scrolling animation that should be used to select
+     * the scroll snap point.
+     */
+    static void RequestFlingSnap(const FrameMetrics::ViewID& aScrollId,
+                                 const mozilla::CSSPoint& aDestination);
+
     /* Tell layout that we received the scroll offset update for the given view ID, so
        that it accepts future scroll offset updates from APZ. */
     static void AcknowledgeScrollUpdate(const FrameMetrics::ViewID& aScrollId,
                                         const uint32_t& aScrollGeneration);
 
     /* Apply an "input transform" to the given |aInput| and return the transformed value.
        The input transform applied is the one for the content element corresponding to
        |aGuid|; this is populated in a previous call to UpdateCallbackTransform. See that
--- a/gfx/layers/apz/util/ChromeProcessController.cpp
+++ b/gfx/layers/apz/util/ChromeProcessController.cpp
@@ -76,16 +76,23 @@ ChromeProcessController::RequestContentR
 
 void
 ChromeProcessController::PostDelayedTask(Task* aTask, int aDelayMs)
 {
   MessageLoop::current()->PostDelayedTask(FROM_HERE, aTask, aDelayMs);
 }
 
 void
+ChromeProcessController::RequestFlingSnap(const FrameMetrics::ViewID& aScrollId,
+                                          const mozilla::CSSPoint& aDestination)
+{
+  APZCCallbackHelper::RequestFlingSnap(aScrollId, aDestination);
+}
+
+void
 ChromeProcessController::AcknowledgeScrollUpdate(const FrameMetrics::ViewID& aScrollId,
                                                  const uint32_t& aScrollGeneration)
 {
   APZCCallbackHelper::AcknowledgeScrollUpdate(aScrollId, aScrollGeneration);
 }
 
 void
 ChromeProcessController::Destroy()
--- a/gfx/layers/apz/util/ChromeProcessController.h
+++ b/gfx/layers/apz/util/ChromeProcessController.h
@@ -32,16 +32,18 @@ class ChromeProcessController : public m
 
 public:
   explicit ChromeProcessController(nsIWidget* aWidget, APZEventState* aAPZEventState);
   virtual void Destroy() MOZ_OVERRIDE;
 
   // GeckoContentController interface
   virtual void RequestContentRepaint(const FrameMetrics& aFrameMetrics) MOZ_OVERRIDE;
   virtual void PostDelayedTask(Task* aTask, int aDelayMs) MOZ_OVERRIDE;
+  virtual void RequestFlingSnap(const FrameMetrics::ViewID& aScrollId,
+                                const mozilla::CSSPoint& aDestination) MOZ_OVERRIDE;
   virtual void AcknowledgeScrollUpdate(const FrameMetrics::ViewID& aScrollId,
                                        const uint32_t& aScrollGeneration) MOZ_OVERRIDE;
 
   virtual void HandleDoubleTap(const mozilla::CSSPoint& aPoint, Modifiers aModifiers,
                                const ScrollableLayerGuid& aGuid) MOZ_OVERRIDE {}
   virtual void HandleSingleTap(const mozilla::CSSPoint& aPoint, Modifiers aModifiers,
                                const ScrollableLayerGuid& aGuid) MOZ_OVERRIDE;
   virtual void HandleLongTap(const mozilla::CSSPoint& aPoint, Modifiers aModifiers,
--- a/gfx/tests/gtest/TestAsyncPanZoomController.cpp
+++ b/gfx/tests/gtest/TestAsyncPanZoomController.cpp
@@ -56,16 +56,17 @@ private:
   ScopedGfxPref<prefType> pref_##prefBase( \
     &(gfxPrefs::prefBase), \
     &(gfxPrefs::Set##prefBase), \
     prefValue)
 
 class MockContentController : public GeckoContentController {
 public:
   MOCK_METHOD1(RequestContentRepaint, void(const FrameMetrics&));
+  MOCK_METHOD2(RequestFlingSnap, void(const FrameMetrics::ViewID& aScrollId, const mozilla::CSSPoint& aDestination));
   MOCK_METHOD2(AcknowledgeScrollUpdate, void(const FrameMetrics::ViewID&, const uint32_t& aScrollGeneration));
   MOCK_METHOD3(HandleDoubleTap, void(const CSSPoint&, Modifiers, const ScrollableLayerGuid&));
   MOCK_METHOD3(HandleSingleTap, void(const CSSPoint&, Modifiers, const ScrollableLayerGuid&));
   MOCK_METHOD4(HandleLongTap, void(const CSSPoint&, Modifiers, const ScrollableLayerGuid&, uint64_t));
   MOCK_METHOD3(HandleLongTapUp, void(const CSSPoint&, Modifiers, const ScrollableLayerGuid&));
   MOCK_METHOD3(SendAsyncScrollDOMEvent, void(bool aIsRoot, const CSSRect &aContentRect, const CSSSize &aScrollableSize));
   MOCK_METHOD2(PostDelayedTask, void(Task* aTask, int aDelayMs));
   MOCK_METHOD3(NotifyAPZStateChange, void(const ScrollableLayerGuid& aGuid, APZStateChange aChange, int aArg));
--- a/layout/base/nsPresShell.cpp
+++ b/layout/base/nsPresShell.cpp
@@ -2387,66 +2387,70 @@ PresShell::ScrollPage(bool aForward)
 {
   nsIScrollableFrame* scrollFrame =
     GetFrameToScrollAsScrollable(nsIPresShell::eVertical);
   if (scrollFrame) {
     scrollFrame->ScrollBy(nsIntPoint(0, aForward ? 1 : -1),
                           nsIScrollableFrame::PAGES,
                           nsIScrollableFrame::SMOOTH,
                           nullptr, nullptr,
-                          nsIScrollableFrame::NOT_MOMENTUM);
+                          nsIScrollableFrame::NOT_MOMENTUM,
+                          nsIScrollableFrame::ENABLE_SNAP);
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 PresShell::ScrollLine(bool aForward)
 {
   nsIScrollableFrame* scrollFrame =
     GetFrameToScrollAsScrollable(nsIPresShell::eVertical);
   if (scrollFrame) {
     int32_t lineCount = Preferences::GetInt("toolkit.scrollbox.verticalScrollDistance",
                                             NS_DEFAULT_VERTICAL_SCROLL_DISTANCE);
     scrollFrame->ScrollBy(nsIntPoint(0, aForward ? lineCount : -lineCount),
                           nsIScrollableFrame::LINES,
                           nsIScrollableFrame::SMOOTH,
                           nullptr, nullptr,
-                          nsIScrollableFrame::NOT_MOMENTUM);
+                          nsIScrollableFrame::NOT_MOMENTUM,
+                          nsIScrollableFrame::ENABLE_SNAP);
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 PresShell::ScrollCharacter(bool aRight)
 {
   nsIScrollableFrame* scrollFrame =
     GetFrameToScrollAsScrollable(nsIPresShell::eHorizontal);
   if (scrollFrame) {
     int32_t h = Preferences::GetInt("toolkit.scrollbox.horizontalScrollDistance",
                                     NS_DEFAULT_HORIZONTAL_SCROLL_DISTANCE);
     scrollFrame->ScrollBy(nsIntPoint(aRight ? h : -h, 0),
                           nsIScrollableFrame::LINES,
                           nsIScrollableFrame::SMOOTH,
                           nullptr, nullptr,
-                          nsIScrollableFrame::NOT_MOMENTUM);
+                          nsIScrollableFrame::NOT_MOMENTUM,
+                          nsIScrollableFrame::ENABLE_SNAP);
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 PresShell::CompleteScroll(bool aForward)
 {
   nsIScrollableFrame* scrollFrame =
     GetFrameToScrollAsScrollable(nsIPresShell::eVertical);
   if (scrollFrame) {
     scrollFrame->ScrollBy(nsIntPoint(0, aForward ? 1 : -1),
                           nsIScrollableFrame::WHOLE,
                           nsIScrollableFrame::SMOOTH,
                           nullptr, nullptr,
-                          nsIScrollableFrame::NOT_MOMENTUM);
+                          nsIScrollableFrame::NOT_MOMENTUM,
+                          nsIScrollableFrame::ENABLE_SNAP);
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 PresShell::CompleteMove(bool aForward, bool aExtend)
 {
   // Beware! This may flush notifications via synchronous
--- a/layout/generic/nsGfxScrollFrame.cpp
+++ b/layout/generic/nsGfxScrollFrame.cpp
@@ -1894,16 +1894,17 @@ ScrollFrameHelper::ScrollFrameHelper(nsC
   , mHasBeenScrolledRecently(false)
   , mCollapsedResizer(false)
   , mShouldBuildScrollableLayer(false)
   , mHasBeenScrolled(false)
   , mIsResolutionSet(false)
   , mIgnoreMomentumScroll(false)
   , mScaleToResolution(false)
   , mTransformingByAPZ(false)
+  , mVelocityQueue(aOuter->PresContext())
 {
   if (LookAndFeel::GetInt(LookAndFeel::eIntID_UseOverlayScrollbars) != 0) {
     mScrollbarActivity = new ScrollbarActivity(do_QueryFrame(aOuter));
   }
 
   EnsureImageVisPrefsCached();
 
   if (IsAlwaysActive() &&
@@ -2064,18 +2065,26 @@ ScrollFrameHelper::GetScrollPositionCSSP
 /*
  * this method wraps calls to ScrollToImpl(), either in one shot or incrementally,
  *  based on the setting of the smoothness scroll pref
  */
 void
 ScrollFrameHelper::ScrollToWithOrigin(nsPoint aScrollPosition,
                                           nsIScrollableFrame::ScrollMode aMode,
                                           nsIAtom *aOrigin,
-                                          const nsRect* aRange)
-{
+                                          const nsRect* aRange,
+                                          nsIScrollableFrame::ScrollSnapMode aSnap)
+{
+
+  if (aSnap == nsIScrollableFrame::ENABLE_SNAP) {
+    GetSnapPointForDestination(nsIScrollableFrame::DEVICE_PIXELS,
+                               mDestination,
+                               aScrollPosition);
+  }
+
   nsRect scrollRange = GetScrollRangeForClamping();
   mDestination = scrollRange.ClampPoint(aScrollPosition);
 
   nsRect range = aRange ? *aRange : nsRect(aScrollPosition, nsSize(0, 0));
 
   if (aMode == nsIScrollableFrame::INSTANT) {
     // Asynchronous scrolling is not allowed, so we'll kill any existing
     // async-scrolling process and do an instant scroll.
@@ -2089,16 +2098,19 @@ ScrollFrameHelper::ScrollToWithOrigin(ns
                           IsSmoothScrollingEnabled();
 
   nsSize currentVelocity(0, 0);
 
   if (gfxPrefs::ScrollBehaviorEnabled()) {
     if (aMode == nsIScrollableFrame::SMOOTH_MSD) {
       mIgnoreMomentumScroll = true;
       if (!mAsyncSmoothMSDScroll) {
+        nsPoint sv = mVelocityQueue.GetVelocity();
+        currentVelocity.width = sv.x;
+        currentVelocity.height = sv.y;
         if (mAsyncScroll) {
           if (mAsyncScroll->mIsSmoothScroll) {
             currentVelocity = mAsyncScroll->VelocityAt(now);
           }
           mAsyncScroll = nullptr;
         }
 
         if (gfxPrefs::AsyncPanZoomEnabled()) {
@@ -3322,17 +3334,18 @@ CalcRangeForScrollBy(int32_t aDelta, nsc
 }
 
 void
 ScrollFrameHelper::ScrollBy(nsIntPoint aDelta,
                             nsIScrollableFrame::ScrollUnit aUnit,
                             nsIScrollableFrame::ScrollMode aMode,
                             nsIntPoint* aOverflow,
                             nsIAtom *aOrigin,
-                            nsIScrollableFrame::ScrollMomentum aMomentum)
+                            nsIScrollableFrame::ScrollMomentum aMomentum,
+                            nsIScrollableFrame::ScrollSnapMode aSnap)
 {
   // When a smooth scroll is being processed on a frame, mouse wheel and trackpad
   // momentum scroll event updates must notcancel the SMOOTH or SMOOTH_MSD
   // scroll animations, enabling Javascript that depends on them to be responsive
   // without forcing the user to wait for the fling animations to completely stop.
   switch (aMomentum) {
   case nsIScrollableFrame::NOT_MOMENTUM:
     mIgnoreMomentumScroll = false;
@@ -3385,30 +3398,52 @@ ScrollFrameHelper::ScrollBy(nsIntPoint a
     negativeTolerance = 0.05f;
     positiveTolerance = 0;
     break;
   }
   case nsIScrollableFrame::WHOLE: {
     nsPoint pos = GetScrollPosition();
     AdjustForWholeDelta(aDelta.x, &pos.x);
     AdjustForWholeDelta(aDelta.y, &pos.y);
+    if (aSnap == nsIScrollableFrame::ENABLE_SNAP) {
+      GetSnapPointForDestination(aUnit, mDestination, pos);
+    }
     ScrollTo(pos, aMode);
     // 'this' might be destroyed here
     if (aOverflow) {
       *aOverflow = nsIntPoint(0, 0);
     }
     return;
   }
   default:
     NS_ERROR("Invalid scroll mode");
     return;
   }
 
-  nsPoint newPos = mDestination +
-    nsPoint(aDelta.x*deltaMultiplier.width, aDelta.y*deltaMultiplier.height);
+  nsPoint newPos = mDestination + nsPoint(aDelta.x*deltaMultiplier.width, aDelta.y*deltaMultiplier.height);
+
+  if (aSnap == nsIScrollableFrame::ENABLE_SNAP) {
+    ScrollbarStyles styles = GetScrollbarStylesFromFrame();
+    if (styles.mScrollSnapTypeY != NS_STYLE_SCROLL_SNAP_TYPE_NONE ||
+        styles.mScrollSnapTypeX != NS_STYLE_SCROLL_SNAP_TYPE_NONE) {
+      nscoord appUnitsPerDevPixel = mOuter->PresContext()->AppUnitsPerDevPixel();
+      deltaMultiplier = nsSize(appUnitsPerDevPixel, appUnitsPerDevPixel);
+      negativeTolerance = 0.1f;
+      positiveTolerance = 0;
+      nsIScrollableFrame::ScrollUnit snapUnit = aUnit;
+      if (aOrigin == nsGkAtoms::mouseWheel) {
+        // When using a clicky scroll wheel, snap point selection works the same
+        // as keyboard up/down/left/right navigation, but with varying amounts
+        // of scroll delta.
+        snapUnit = nsIScrollableFrame::LINES;
+      }
+      GetSnapPointForDestination(snapUnit, mDestination, newPos);
+    }
+  }
+
   // Calculate desired range values.
   nscoord rangeLowerX, rangeUpperX, rangeLowerY, rangeUpperY;
   CalcRangeForScrollBy(aDelta.x, newPos.x, negativeTolerance, positiveTolerance,
                        deltaMultiplier.width, &rangeLowerX, &rangeUpperX);
   CalcRangeForScrollBy(aDelta.y, newPos.y, negativeTolerance, positiveTolerance,
                        deltaMultiplier.height, &rangeLowerY, &rangeUpperY);
   nsRect range(rangeLowerX,
                rangeLowerY,
@@ -3422,16 +3457,59 @@ ScrollFrameHelper::ScrollBy(nsIntPoint a
 
   if (aOverflow) {
     nsPoint clampAmount = newPos - mDestination;
     float appUnitsPerDevPixel = mOuter->PresContext()->AppUnitsPerDevPixel();
     *aOverflow = nsIntPoint(
         NSAppUnitsToIntPixels(clampAmount.x, appUnitsPerDevPixel),
         NSAppUnitsToIntPixels(clampAmount.y, appUnitsPerDevPixel));
   }
+
+  if (aUnit == nsIScrollableFrame::DEVICE_PIXELS &&
+      !gfxPrefs::AsyncPanZoomEnabled()) {
+    // When APZ is disabled, we must track the velocity
+    // on the main thread; otherwise, the APZC will manage this.
+    mVelocityQueue.Sample(GetScrollPosition());
+  }
+}
+
+void
+ScrollFrameHelper::ScrollSnap()
+{
+  float flingSensitivity = gfxPrefs::ScrollSnapPredictionSensitivity();
+  int maxVelocity = gfxPrefs::ScrollSnapPredictionMaxVelocity();
+  maxVelocity = nsPresContext::CSSPixelsToAppUnits(maxVelocity);
+  int maxOffset = maxVelocity * flingSensitivity;
+  nsPoint velocity = mVelocityQueue.GetVelocity();
+  // Multiply each component individually to avoid integer multiply
+  nsPoint predictedOffset = nsPoint(velocity.x * flingSensitivity,
+                                    velocity.y * flingSensitivity);
+  predictedOffset.Clamp(maxOffset);
+  nsPoint pos = GetScrollPosition();
+  nsPoint destinationPos = pos + predictedOffset;
+  ScrollSnap(destinationPos);
+}
+
+void
+ScrollFrameHelper::FlingSnap(const mozilla::CSSPoint& aDestination)
+{
+  ScrollSnap(CSSPoint::ToAppUnits(aDestination));
+}
+
+void
+ScrollFrameHelper::ScrollSnap(const nsPoint &aDestination)
+{
+  nsRect scrollRange = GetScrollRangeForClamping();
+  nsPoint pos = GetScrollPosition();
+  nsPoint snapDestination = scrollRange.ClampPoint(aDestination);
+  if (GetSnapPointForDestination(nsIScrollableFrame::DEVICE_PIXELS,
+                                                 pos,
+                                                 snapDestination)) {
+    ScrollTo(snapDestination, nsIScrollableFrame::SMOOTH_MSD);
+  }
 }
 
 nsSize
 ScrollFrameHelper::GetLineScrollAmount() const
 {
   nsRefPtr<nsFontMetrics> fm;
   nsLayoutUtils::GetFontMetricsForFrame(mOuter, getter_AddRefs(fm),
     nsLayoutUtils::FontSizeInflationFor(mOuter));
@@ -5191,8 +5269,348 @@ nsIScrollableFrame::GetPerceivedScrollin
   if (scrollRange.width >= oneDevPixel) {
     directions |= HORIZONTAL;
   }
   if (scrollRange.height >= oneDevPixel) {
     directions |= VERTICAL;
   }
   return directions;
 }
+
+/**
+ * Stores candidate snapping edges.
+ */
+class SnappingEdgeCallback {
+public:
+  virtual void AddHorizontalEdge(nscoord aEdge) = 0;
+  virtual void AddVerticalEdge(nscoord aEdge) = 0;
+  virtual void AddHorizontalEdgeInterval(const nsRect &aScrollRange,
+                                         nscoord aInterval,
+                                         nscoord aOffset) = 0;
+  virtual void AddVerticalEdgeInterval(const nsRect &aScrollRange,
+                                       nscoord aInterval,
+                                       nscoord aOffset) = 0;
+};
+
+/**
+ * Keeps track of the current best edge to snap to. The criteria for
+ * adding an edge depends on the scrolling unit.
+ */
+class CalcSnapPoints : public SnappingEdgeCallback {
+public:
+  CalcSnapPoints(nsIScrollableFrame::ScrollUnit aUnit,
+                 const nsPoint& aDestination,
+                 const nsPoint& aStartPos);
+  virtual void AddHorizontalEdge(nscoord aEdge) MOZ_OVERRIDE;
+  virtual void AddVerticalEdge(nscoord aEdge) MOZ_OVERRIDE;
+  virtual void AddHorizontalEdgeInterval(const nsRect &aScrollRange,
+                                         nscoord aInterval, nscoord aOffset)
+                                         MOZ_OVERRIDE;
+  virtual void AddVerticalEdgeInterval(const nsRect &aScrollRange,
+                                       nscoord aInterval, nscoord aOffset)
+                                       MOZ_OVERRIDE;
+  void AddEdge(nscoord aEdge,
+               nscoord aDestination,
+               nscoord aStartPos,
+               nscoord aScrollingDirection,
+               nscoord* aBestEdge,
+               bool* aEdgeFound);
+  void AddEdgeInterval(nscoord aInterval,
+                       nscoord aMinPos,
+                       nscoord aMaxPos,
+                       nscoord aOffset,
+                       nscoord aDestination,
+                       nscoord aStartPos,
+                       nscoord aScrollingDirection,
+                       nscoord* aBestEdge,
+                       bool* aEdgeFound);
+  nsPoint GetBestEdge() const;
+protected:
+  nsIScrollableFrame::ScrollUnit mUnit;
+  nsPoint mDestination;            // gives the position after scrolling but before snapping
+  nsPoint mStartPos;               // gives the position before scrolling
+  nsIntPoint mScrollingDirection;  // always -1, 0, or 1
+  nsPoint mBestEdge;               // keeps track of the position of the current best edge
+  bool mHorizontalEdgeFound;       // true if mBestEdge.x is storing a valid horizontal edge
+  bool mVerticalEdgeFound;         // true if mBestEdge.y is storing a valid vertical edge
+};
+
+CalcSnapPoints::CalcSnapPoints(nsIScrollableFrame::ScrollUnit aUnit,
+                               const nsPoint& aDestination,
+                               const nsPoint& aStartPos)
+{
+  mUnit = aUnit;
+  mDestination = aDestination;
+  mStartPos = aStartPos;
+
+  nsPoint direction = aDestination - aStartPos;
+  mScrollingDirection = nsIntPoint(0,0);
+  if (direction.x < 0) {
+    mScrollingDirection.x = -1;
+  }
+  if (direction.x > 0) {
+    mScrollingDirection.x = 1;
+  }
+  if (direction.y < 0) {
+    mScrollingDirection.y = -1;
+  }
+  if (direction.y > 0) {
+    mScrollingDirection.y = 1;
+  }
+  mBestEdge = aDestination;
+  mHorizontalEdgeFound = false;
+  mVerticalEdgeFound = false;
+}
+
+nsPoint
+CalcSnapPoints::GetBestEdge() const
+{
+  return nsPoint(mVerticalEdgeFound ? mBestEdge.x : mStartPos.x,
+                 mHorizontalEdgeFound ? mBestEdge.y : mStartPos.y);
+}
+
+void
+CalcSnapPoints::AddHorizontalEdge(nscoord aEdge)
+{
+  AddEdge(aEdge, mDestination.y, mStartPos.y, mScrollingDirection.y, &mBestEdge.y,
+          &mHorizontalEdgeFound);
+}
+
+void
+CalcSnapPoints::AddVerticalEdge(nscoord aEdge)
+{
+  AddEdge(aEdge, mDestination.x, mStartPos.x, mScrollingDirection.x, &mBestEdge.x,
+          &mVerticalEdgeFound);
+}
+
+void
+CalcSnapPoints::AddHorizontalEdgeInterval(const nsRect &aScrollRange,
+                                          nscoord aInterval, nscoord aOffset)
+{
+  AddEdgeInterval(aInterval, aScrollRange.y, aScrollRange.YMost(), aOffset,
+                  mDestination.y, mStartPos.y, mScrollingDirection.y,
+                  &mBestEdge.y, &mHorizontalEdgeFound);
+}
+
+void
+CalcSnapPoints::AddVerticalEdgeInterval(const nsRect &aScrollRange,
+                                        nscoord aInterval, nscoord aOffset)
+{
+  AddEdgeInterval(aInterval, aScrollRange.x, aScrollRange.XMost(), aOffset,
+                  mDestination.x, mStartPos.x, mScrollingDirection.x,
+                  &mBestEdge.x, &mVerticalEdgeFound);
+}
+
+void
+CalcSnapPoints::AddEdge(nscoord aEdge, nscoord aDestination, nscoord aStartPos,
+                        nscoord aScrollingDirection, nscoord* aBestEdge,
+                        bool *aEdgeFound)
+{
+  // nsIScrollableFrame::DEVICE_PIXELS indicates that we are releasing a drag
+  // gesture or any other user input event that sets an absolute scroll
+  // position.  In this case, scroll snapping is expected to travel in any
+  // direction.  Otherwise, we will restrict the direction of the scroll
+  // snapping movement based on aScrollingDirection.
+  if (mUnit != nsIScrollableFrame::DEVICE_PIXELS) {
+    // Unless DEVICE_PIXELS, we only want to snap to points ahead of the
+    // direction we are scrolling
+    if (aScrollingDirection == 0) {
+      // The scroll direction is neutral - will not hit a snap point.
+      return;
+    }
+    // nsIScrollableFrame::WHOLE indicates that we are navigating to "home" or
+    // "end".  In this case, we will always select the first or last snap point
+    // regardless of the direction of the scroll.  Otherwise, we will select
+    // scroll snapping points only in the direction specified by
+    // aScrollingDirection.
+    if (mUnit != nsIScrollableFrame::WHOLE) {
+      // Direction of the edge from the current position (before scrolling) in
+      // the direction of scrolling
+      nscoord direction = (aEdge - aStartPos) * aScrollingDirection;
+      if (direction <= 0) {
+        // The edge is not in the direction we are scrolling, skip it.
+        return;
+      }
+    }
+  }
+  if (!*aEdgeFound) {
+    *aBestEdge = aEdge;
+    *aEdgeFound = true;
+    return;
+  }
+  if (mUnit == nsIScrollableFrame::DEVICE_PIXELS ||
+      mUnit == nsIScrollableFrame::LINES) {
+    if (std::abs(aEdge - aDestination) < std::abs(*aBestEdge - aDestination)) {
+      *aBestEdge = aEdge;
+    }
+  } else if (mUnit == nsIScrollableFrame::PAGES) {
+    // distance to the edge from the scrolling destination in the direction of scrolling
+    nscoord overshoot = (aEdge - aDestination) * aScrollingDirection;
+    // distance to the current best edge from the scrolling destination in the direction of scrolling
+    nscoord curOvershoot = (*aBestEdge - aDestination) * aScrollingDirection;
+
+    // edges between the current position and the scrolling destination are favoured
+    // to preserve context
+    if (overshoot < 0 && (overshoot > curOvershoot || curOvershoot >= 0)) {
+      *aBestEdge = aEdge;
+    }
+    // if there are no edges between the current position and the scrolling destination
+    // the closest edge beyond the destination is used
+    if (overshoot > 0 && overshoot < curOvershoot) {
+      *aBestEdge = aEdge;
+    }
+  } else if (mUnit == nsIScrollableFrame::WHOLE) {
+    // the edge closest to the top/bottom/left/right is used, depending on scrolling direction
+    if (aScrollingDirection > 0 && aEdge > *aBestEdge) {
+      *aBestEdge = aEdge;
+    } else if (aScrollingDirection < 0 && aEdge < *aBestEdge) {
+      *aBestEdge = aEdge;
+    }
+  } else {
+    NS_ERROR("Invalid scroll mode");
+    return;
+  }
+}
+
+void
+CalcSnapPoints::AddEdgeInterval(nscoord aInterval, nscoord aMinPos,
+                                nscoord aMaxPos, nscoord aOffset,
+                                nscoord aDestination, nscoord aStartPos,
+                                nscoord aScrollingDirection,
+                                nscoord* aBestEdge, bool *aEdgeFound)
+{
+  if (aInterval == 0) {
+    // When interval is 0, there are no scroll snap points.
+    // Avoid division by zero and bail.
+    return;
+  }
+
+  // The only possible candidate interval snap points are the edges immediately
+  // surrounding aDestination.
+
+  // aDestination must be clamped to the scroll
+  // range in order to handle cases where the best matching snap point would
+  // result in scrolling out of bounds.  This clamping must be prior to
+  // selecting the two interval edges.
+  nscoord clamped = std::max(std::min(aDestination, aMaxPos), aMinPos);
+
+  // Add each edge in the interval immediately before aTarget and after aTarget
+  // Do not add edges that are out of range.
+  nscoord r = (clamped + aOffset) % aInterval;
+  if (r < aMinPos) {
+    r += aInterval;
+  }
+  nscoord edge = clamped - r;
+  if (edge >= aMinPos && edge <= aMaxPos) {
+    AddEdge(edge, aDestination, aStartPos, aScrollingDirection, aBestEdge,
+            aEdgeFound);
+  }
+  edge += aInterval;
+  if (edge >= aMinPos && edge <= aMaxPos) {
+    AddEdge(edge, aDestination, aStartPos, aScrollingDirection, aBestEdge,
+            aEdgeFound);
+  }
+}
+
+static void
+ScrollSnapHelper(SnappingEdgeCallback& aCallback, nsIFrame* aFrame,
+                 nsIFrame* aScrolledFrame,
+                 const nsPoint &aScrollSnapDestination) {
+  nsIFrame::ChildListIterator childLists(aFrame);
+  for (; !childLists.IsDone(); childLists.Next()) {
+    nsFrameList::Enumerator childFrames(childLists.CurrentList());
+    for (; !childFrames.AtEnd(); childFrames.Next()) {
+      nsIFrame* f = childFrames.get();
+
+      const nsStyleDisplay* styleDisplay = f->StyleDisplay();
+      size_t coordCount = styleDisplay->mScrollSnapCoordinate.Length();
+
+      if (coordCount) {
+        nsRect frameRect = f->GetRect();
+        nsPoint offset = f->GetOffsetTo(aScrolledFrame);
+        nsRect edgesRect = nsRect(offset, frameRect.Size());
+        for (size_t coordNum = 0; coordNum < coordCount; coordNum++) {
+          const nsStyleBackground::Position &coordPosition =
+            f->StyleDisplay()->mScrollSnapCoordinate[coordNum];
+          nsPoint coordPoint = edgesRect.TopLeft() - aScrollSnapDestination;
+          coordPoint += nsPoint(coordPosition.mXPosition.mLength,
+                                coordPosition.mYPosition.mLength);
+          if (coordPosition.mXPosition.mHasPercent) {
+            coordPoint.x += NSToCoordRound(coordPosition.mXPosition.mPercent *
+                                           frameRect.width);
+          }
+          if (coordPosition.mYPosition.mHasPercent) {
+            coordPoint.y += NSToCoordRound(coordPosition.mYPosition.mPercent *
+                                           frameRect.height);
+          }
+
+          aCallback.AddVerticalEdge(coordPoint.x);
+          aCallback.AddHorizontalEdge(coordPoint.y);
+        }
+      }
+
+      ScrollSnapHelper(aCallback, f, aScrolledFrame, aScrollSnapDestination);
+    }
+  }
+}
+
+bool
+ScrollFrameHelper::GetSnapPointForDestination(nsIScrollableFrame::ScrollUnit aUnit,
+                                              nsPoint aStartPos,
+                                              nsPoint &aDestination)
+{
+  ScrollbarStyles styles = GetScrollbarStylesFromFrame();
+  if (styles.mScrollSnapTypeY == NS_STYLE_SCROLL_SNAP_TYPE_NONE &&
+      styles.mScrollSnapTypeX == NS_STYLE_SCROLL_SNAP_TYPE_NONE) {
+    return false;
+  }
+
+  nsSize scrollPortSize = mScrollPort.Size();
+  nsRect scrollRange = GetScrollRangeForClamping();
+
+  nsPoint destPos = nsPoint(styles.mScrollSnapDestinationX.mLength,
+                            styles.mScrollSnapDestinationY.mLength);
+  if (styles.mScrollSnapDestinationX.mHasPercent) {
+    destPos.x += NSToCoordFloorClamped(styles.mScrollSnapDestinationX.mPercent
+                                       * scrollPortSize.width);
+  }
+
+  if (styles.mScrollSnapDestinationY.mHasPercent) {
+    destPos.y += NSToCoordFloorClamped(styles.mScrollSnapDestinationY.mPercent
+                                       * scrollPortSize.height);
+  }
+
+  CalcSnapPoints calcSnapPoints(aUnit, aDestination, aStartPos);
+
+  if (styles.mScrollSnapPointsX.GetUnit() != eStyleUnit_None) {
+    nscoord interval = nsRuleNode::ComputeCoordPercentCalc(styles.mScrollSnapPointsX,
+                                                           scrollPortSize.width);
+    calcSnapPoints.AddVerticalEdgeInterval(scrollRange, interval, destPos.x);
+  }
+  if (styles.mScrollSnapPointsY.GetUnit() != eStyleUnit_None) {
+    nscoord interval = nsRuleNode::ComputeCoordPercentCalc(styles.mScrollSnapPointsY,
+                                                           scrollPortSize.width);
+    calcSnapPoints.AddHorizontalEdgeInterval(scrollRange, interval, destPos.y);
+  }
+
+  ScrollSnapHelper(calcSnapPoints, mScrolledFrame, mScrolledFrame, destPos);
+  bool snapped = false;
+  nsPoint finalPos = calcSnapPoints.GetBestEdge();
+  nscoord proximityThreshold =
+    Preferences::GetInt("layout.css.scroll-snap.proximity-threshold", 0);
+  proximityThreshold = nsPresContext::CSSPixelsToAppUnits(proximityThreshold);
+  if (styles.mScrollSnapTypeY == NS_STYLE_SCROLL_SNAP_TYPE_PROXIMITY &&
+      std::abs(aDestination.y - finalPos.y) > proximityThreshold) {
+    finalPos.y = aDestination.y;
+  } else {
+    snapped = true;
+  }
+  if (styles.mScrollSnapTypeX == NS_STYLE_SCROLL_SNAP_TYPE_PROXIMITY &&
+      std::abs(aDestination.x - finalPos.x) > proximityThreshold) {
+    finalPos.x = aDestination.x;
+  } else {
+    snapped = true;
+  }
+  if (snapped) {
+    aDestination = finalPos;
+  }
+  return snapped;
+}
--- a/layout/generic/nsGfxScrollFrame.h
+++ b/layout/generic/nsGfxScrollFrame.h
@@ -16,16 +16,17 @@
 #include "nsIScrollbarMediator.h"
 #include "nsIStatefulFrame.h"
 #include "nsThreadUtils.h"
 #include "nsIReflowCallback.h"
 #include "nsBoxLayoutState.h"
 #include "nsQueryFrame.h"
 #include "nsExpirationTracker.h"
 #include "TextOverflow.h"
+#include "ScrollVelocityQueue.h"
 
 class nsPresContext;
 class nsIPresShell;
 class nsIContent;
 class nsIAtom;
 class nsIScrollFrameInternal;
 class nsPresState;
 class nsIScrollPositionListener;
@@ -174,34 +175,39 @@ public:
   }
   nsRect GetScrollRange() const;
   // Get the scroll range assuming the scrollport has size (aWidth, aHeight).
   nsRect GetScrollRange(nscoord aWidth, nscoord aHeight) const;
   nsSize GetScrollPositionClampingScrollPortSize() const;
   gfxSize GetResolution() const;
   void SetResolution(const gfxSize& aResolution);
   void SetResolutionAndScaleTo(const gfxSize& aResolution);
+  void FlingSnap(const mozilla::CSSPoint& aDestination);
+  void ScrollSnap();
+  void ScrollSnap(const nsPoint &aDestination);
 
 protected:
   nsRect GetScrollRangeForClamping() const;
 
 public:
   static void AsyncScrollCallback(ScrollFrameHelper* aInstance,
                                   mozilla::TimeStamp aTime);
   static void AsyncSmoothMSDScrollCallback(ScrollFrameHelper* aInstance,
                                            mozilla::TimeDuration aDeltaTime);
   /**
    * @note This method might destroy the frame, pres shell and other objects.
    * aRange is the range of allowable scroll positions around the desired
    * aScrollPosition. Null means only aScrollPosition is allowed.
    * This is a closed-ended range --- aRange.XMost()/aRange.YMost() are allowed.
    */
   void ScrollTo(nsPoint aScrollPosition, nsIScrollableFrame::ScrollMode aMode,
-                const nsRect* aRange = nullptr) {
-    ScrollToWithOrigin(aScrollPosition, aMode, nsGkAtoms::other, aRange);
+                const nsRect* aRange = nullptr,
+                nsIScrollableFrame::ScrollSnapMode aSnap = nsIScrollableFrame::DISABLE_SNAP) {
+    ScrollToWithOrigin(aScrollPosition, aMode, nsGkAtoms::other, aRange,
+                       aSnap);
   }
   /**
    * @note This method might destroy the frame, pres shell and other objects.
    */
   void ScrollToCSSPixels(const CSSIntPoint& aScrollPosition,
                          nsIScrollableFrame::ScrollMode aMode
                            = nsIScrollableFrame::INSTANT);
   /**
@@ -217,21 +223,33 @@ public:
   void ScrollToImpl(nsPoint aScrollPosition, const nsRect& aRange, nsIAtom* aOrigin = nullptr);
   void ScrollVisual(nsPoint aOldScrolledFramePosition);
   /**
    * @note This method might destroy the frame, pres shell and other objects.
    */
   void ScrollBy(nsIntPoint aDelta, nsIScrollableFrame::ScrollUnit aUnit,
                 nsIScrollableFrame::ScrollMode aMode, nsIntPoint* aOverflow,
                 nsIAtom* aOrigin = nullptr,
-                nsIScrollableFrame::ScrollMomentum aMomentum = nsIScrollableFrame::NOT_MOMENTUM);
+                nsIScrollableFrame::ScrollMomentum aMomentum = nsIScrollableFrame::NOT_MOMENTUM,
+                nsIScrollableFrame::ScrollSnapMode aSnap = nsIScrollableFrame::DISABLE_SNAP);
   /**
    * @note This method might destroy the frame, pres shell and other objects.
    */
   void ScrollToRestoredPosition();
+  /**
+   * GetSnapPointForDestination determines which point to snap to after
+   * scrolling. aStartPos gives the position before scrolling and aDestination
+   * gives the position after scrolling, with no snapping. Behaviour is
+   * dependent on the value of aUnit.
+   * Returns true if a suitable snap point could be found and aDestination has
+   * been updated to a valid snapping position.
+   */
+  bool GetSnapPointForDestination(nsIScrollableFrame::ScrollUnit aUnit,
+                                  nsPoint aStartPos,
+                                  nsPoint &aDestination);
 
   nsSize GetLineScrollAmount() const;
   nsSize GetPageScrollAmount() const;
 
   nsPresState* SaveState() const;
   void RestoreState(nsPresState* aState);
 
   nsIFrame* GetScrolledFrame() const { return mScrolledFrame; }
@@ -487,24 +505,27 @@ public:
   // True if the frame's resolution has been set via SetResolutionAndScaleTo.
   // Only meaningful for root scroll frames.
   bool mScaleToResolution:1;
 
   // True if the APZ is in the process of async-transforming this scrollframe,
   // (as best as we can tell on the main thread, anyway).
   bool mTransformingByAPZ:1;
 
+  mozilla::layout::ScrollVelocityQueue mVelocityQueue;
+
 protected:
   /**
    * @note This method might destroy the frame, pres shell and other objects.
    */
   void ScrollToWithOrigin(nsPoint aScrollPosition,
                           nsIScrollableFrame::ScrollMode aMode,
                           nsIAtom *aOrigin, // nullptr indicates "other" origin
-                          const nsRect* aRange);
+                          const nsRect* aRange,
+                          nsIScrollableFrame::ScrollSnapMode aSnap = nsIScrollableFrame::DISABLE_SNAP);
 
   void CompleteAsyncScroll(const nsRect &aRange, nsIAtom* aOrigin = nullptr);
 
   static void EnsureImageVisPrefsCached();
   static bool sImageVisPrefsCached;
   // The number of scrollports wide/high to expand when looking for images.
   static uint32_t sHorzExpandScrollPort;
   static uint32_t sVertExpandScrollPort;
@@ -682,18 +703,20 @@ public:
   }
   virtual nsSize GetPageScrollAmount() const MOZ_OVERRIDE {
     return mHelper.GetPageScrollAmount();
   }
   /**
    * @note This method might destroy the frame, pres shell and other objects.
    */
   virtual void ScrollTo(nsPoint aScrollPosition, ScrollMode aMode,
-                        const nsRect* aRange = nullptr) MOZ_OVERRIDE {
-    mHelper.ScrollTo(aScrollPosition, aMode, aRange);
+                        const nsRect* aRange = nullptr,
+                        nsIScrollableFrame::ScrollSnapMode aSnap = nsIScrollableFrame::DISABLE_SNAP)
+                        MOZ_OVERRIDE {
+    mHelper.ScrollTo(aScrollPosition, aMode, aRange, aSnap);
   }
   /**
    * @note This method might destroy the frame, pres shell and other objects.
    */
   virtual void ScrollToCSSPixels(const CSSIntPoint& aScrollPosition,
                                  nsIScrollableFrame::ScrollMode aMode
                                    = nsIScrollableFrame::INSTANT) MOZ_OVERRIDE {
     mHelper.ScrollToCSSPixels(aScrollPosition, aMode);
@@ -708,19 +731,26 @@ public:
   virtual CSSIntPoint GetScrollPositionCSSPixels() MOZ_OVERRIDE {
     return mHelper.GetScrollPositionCSSPixels();
   }
   /**
    * @note This method might destroy the frame, pres shell and other objects.
    */
   virtual void ScrollBy(nsIntPoint aDelta, ScrollUnit aUnit, ScrollMode aMode,
                         nsIntPoint* aOverflow, nsIAtom* aOrigin = nullptr,
-                        nsIScrollableFrame::ScrollMomentum aMomentum = nsIScrollableFrame::NOT_MOMENTUM)
+                        nsIScrollableFrame::ScrollMomentum aMomentum = nsIScrollableFrame::NOT_MOMENTUM,
+                        nsIScrollableFrame::ScrollSnapMode aSnap = nsIScrollableFrame::DISABLE_SNAP)
                         MOZ_OVERRIDE {
-    mHelper.ScrollBy(aDelta, aUnit, aMode, aOverflow, aOrigin, aMomentum);
+    mHelper.ScrollBy(aDelta, aUnit, aMode, aOverflow, aOrigin, aMomentum, aSnap);
+  }
+  virtual void FlingSnap(const mozilla::CSSPoint& aDestination) MOZ_OVERRIDE {
+    mHelper.FlingSnap(aDestination);
+  }
+  virtual void ScrollSnap() MOZ_OVERRIDE {
+    mHelper.ScrollSnap();
   }
   /**
    * @note This method might destroy the frame, pres shell and other objects.
    */
   virtual void ScrollToRestoredPosition() MOZ_OVERRIDE {
     mHelper.ScrollToRestoredPosition();
   }
   virtual void AddScrollPositionListener(nsIScrollPositionListener* aListener) MOZ_OVERRIDE {
@@ -1054,18 +1084,19 @@ public:
   }
   virtual nsSize GetPageScrollAmount() const MOZ_OVERRIDE {
     return mHelper.GetPageScrollAmount();
   }
   /**
    * @note This method might destroy the frame, pres shell and other objects.
    */
   virtual void ScrollTo(nsPoint aScrollPosition, ScrollMode aMode,
-                        const nsRect* aRange = nullptr) MOZ_OVERRIDE {
-    mHelper.ScrollTo(aScrollPosition, aMode, aRange);
+                        const nsRect* aRange = nullptr,
+                        ScrollSnapMode aSnap = nsIScrollableFrame::DISABLE_SNAP) MOZ_OVERRIDE {
+    mHelper.ScrollTo(aScrollPosition, aMode, aRange, aSnap);
   }
   /**
    * @note This method might destroy the frame, pres shell and other objects.
    */
   virtual void ScrollToCSSPixels(const CSSIntPoint& aScrollPosition,
                                  nsIScrollableFrame::ScrollMode aMode
                                    = nsIScrollableFrame::INSTANT) MOZ_OVERRIDE {
     mHelper.ScrollToCSSPixels(aScrollPosition, aMode);
@@ -1077,19 +1108,26 @@ public:
   virtual CSSIntPoint GetScrollPositionCSSPixels() MOZ_OVERRIDE {
     return mHelper.GetScrollPositionCSSPixels();
   }
   /**
    * @note This method might destroy the frame, pres shell and other objects.
    */
   virtual void ScrollBy(nsIntPoint aDelta, ScrollUnit aUnit, ScrollMode aMode,
                         nsIntPoint* aOverflow, nsIAtom* aOrigin = nullptr,
-                        nsIScrollableFrame::ScrollMomentum aMomentum = nsIScrollableFrame::NOT_MOMENTUM)
+                        nsIScrollableFrame::ScrollMomentum aMomentum = nsIScrollableFrame::NOT_MOMENTUM,
+                        nsIScrollableFrame::ScrollSnapMode aSnap = nsIScrollableFrame::DISABLE_SNAP)
                         MOZ_OVERRIDE {
-    mHelper.ScrollBy(aDelta, aUnit, aMode, aOverflow, aOrigin, aMomentum);
+    mHelper.ScrollBy(aDelta, aUnit, aMode, aOverflow, aOrigin, aMomentum, aSnap);
+  }
+  virtual void FlingSnap(const mozilla::CSSPoint& aDestination) MOZ_OVERRIDE {
+    mHelper.FlingSnap(aDestination);
+  }
+  virtual void ScrollSnap() MOZ_OVERRIDE {
+    mHelper.ScrollSnap();
   }
   /**
    * @note This method might destroy the frame, pres shell and other objects.
    */
   virtual void ScrollToRestoredPosition() MOZ_OVERRIDE {
     mHelper.ScrollToRestoredPosition();
   }
   virtual void AddScrollPositionListener(nsIScrollPositionListener* aListener) MOZ_OVERRIDE {
--- a/layout/generic/nsIScrollableFrame.h
+++ b/layout/generic/nsIScrollableFrame.h
@@ -203,26 +203,34 @@ public:
    * after the user has stopped scrolling, simulating a momentum scrolling
    * effect resulting from fling gestures.
    * SYNTHESIZED_MOMENTUM_EVENT indicates that the scrolling is being requested
    * by such a synthesized event and may be ignored if another scroll has
    * been started since the last actual user input.
    */
   enum ScrollMomentum { NOT_MOMENTUM, SYNTHESIZED_MOMENTUM_EVENT };
   /**
+   * When set to ENABLE_SNAP, additional scrolling will be performed after the
+   * scroll operation to maintain the constraints set by CSS Scroll snapping.
+   * The additional scrolling may include asynchronous smooth scrolls that
+   * continue to animate after the initial scroll position has been set.
+   */
+  enum ScrollSnapMode { DISABLE_SNAP, ENABLE_SNAP };
+  /**
    * @note This method might destroy the frame, pres shell and other objects.
    * Clamps aScrollPosition to GetScrollRange and sets the scroll position
    * to that value.
    * @param aRange If non-null, specifies area which contains aScrollPosition
    * and can be used for choosing a performance-optimized scroll position.
    * Any point within this area can be chosen.
    * The choosen point will be as close as possible to aScrollPosition.
    */
   virtual void ScrollTo(nsPoint aScrollPosition, ScrollMode aMode,
-                        const nsRect* aRange = nullptr) = 0;
+                        const nsRect* aRange = nullptr,
+                        ScrollSnapMode aSnap = DISABLE_SNAP) = 0;
   /**
    * @note This method might destroy the frame, pres shell and other objects.
    * Scrolls to a particular position in integer CSS pixels.
    * Keeps the exact current horizontal or vertical position if the current
    * position, rounded to CSS pixels, matches aScrollPosition. If
    * aScrollPosition.x/y is different from the current CSS pixel position,
    * makes sure we only move in the direction given by the difference.
    *
@@ -266,17 +274,37 @@ public:
    * @param aOverflow if non-null, returns the amount that scrolling
    * was clamped by in each direction (how far we moved the scroll position
    * to bring it back into the legal range). This is never negative. The
    * values are in device pixels.
    */
   virtual void ScrollBy(nsIntPoint aDelta, ScrollUnit aUnit, ScrollMode aMode,
                         nsIntPoint* aOverflow = nullptr,
                         nsIAtom* aOrigin = nullptr,
-                        ScrollMomentum aMomentum = NOT_MOMENTUM) = 0;
+                        ScrollMomentum aMomentum = NOT_MOMENTUM,
+                        ScrollSnapMode aSnap = DISABLE_SNAP) = 0;
+
+  /**
+   * Perform scroll snapping, possibly resulting in a smooth scroll to
+   * maintain the scroll snap position constraints.  A predicted landing
+   * position determined by the APZC is used to select the best matching
+   * snap point, allowing touchscreen fling gestures to navigate between
+   * snap points.
+   * @param aDestination The desired landing position of the fling, which
+   * is used to select the best matching snap point.
+   */
+  virtual void FlingSnap(const mozilla::CSSPoint& aDestination) = 0;
+  /**
+   * Perform scroll snapping, possibly resulting in a smooth scroll to
+   * maintain the scroll snap position constraints.  Velocity sampled from
+   * main thread scrolling is used to determine best matching snap point
+   * when called after a fling gesture on a trackpad or mouse wheel.
+   */
+  virtual void ScrollSnap() = 0;
+
   /**
    * @note This method might destroy the frame, pres shell and other objects.
    * This tells the scroll frame to try scrolling to the scroll
    * position that was restored from the history. This must be called
    * at least once after state has been restored. It is called by the
    * scrolled frame itself during reflow, but sometimes state can be
    * restored after reflows are done...
    * XXX should we take an aMode parameter here? Currently it's instant.
--- a/layout/ipc/RenderFrameParent.cpp
+++ b/layout/ipc/RenderFrameParent.cpp
@@ -96,16 +96,34 @@ public:
   {
     MOZ_ASSERT(NS_IsMainThread());
     if (mRenderFrame) {
       TabParent* browser = TabParent::GetFrom(mRenderFrame->Manager());
       browser->UpdateFrame(aFrameMetrics);
     }
   }
 
+  virtual void RequestFlingSnap(const FrameMetrics::ViewID& aScrollId,
+                                const mozilla::CSSPoint& aDestination) MOZ_OVERRIDE
+  {
+    if (MessageLoop::current() != mUILoop) {
+      // We have to send this message from the "UI thread" (main
+      // thread).
+      mUILoop->PostTask(
+        FROM_HERE,
+        NewRunnableMethod(this, &RemoteContentController::RequestFlingSnap,
+                          aScrollId, aDestination));
+      return;
+    }
+    if (mRenderFrame) {
+      TabParent* browser = TabParent::GetFrom(mRenderFrame->Manager());
+      browser->RequestFlingSnap(aScrollId, aDestination);
+    }
+  }
+
   virtual void AcknowledgeScrollUpdate(const FrameMetrics::ViewID& aScrollId,
                                        const uint32_t& aScrollGeneration) MOZ_OVERRIDE
   {
     if (MessageLoop::current() != mUILoop) {
       // We have to send this message from the "UI thread" (main
       // thread).
       mUILoop->PostTask(
         FROM_HERE,
--- a/widget/android/APZCCallbackHandler.cpp
+++ b/widget/android/APZCCallbackHandler.cpp
@@ -98,16 +98,23 @@ APZCCallbackHandler::RequestContentRepai
         if (content) {
             FrameMetrics newSubFrameMetrics(aFrameMetrics);
             APZCCallbackHelper::UpdateSubFrame(content, newSubFrameMetrics);
         }
     }
 }
 
 void
+APZCCallbackHandler::RequestFlingSnap(const FrameMetrics::ViewID& aScrollId,
+                                      const mozilla::CSSPoint& aDestination)
+{
+    APZCCallbackHelper::RequestFlingSnap(aScrollId, aDestination);
+}
+
+void
 APZCCallbackHandler::AcknowledgeScrollUpdate(const FrameMetrics::ViewID& aScrollId,
                                              const uint32_t& aScrollGeneration)
 {
     APZCCallbackHelper::AcknowledgeScrollUpdate(aScrollId, aScrollGeneration);
 }
 
 void
 APZCCallbackHandler::HandleDoubleTap(const CSSPoint& aPoint,
--- a/widget/android/APZCCallbackHandler.h
+++ b/widget/android/APZCCallbackHandler.h
@@ -39,16 +39,18 @@ public:
         return sInstance.get();
     }
 
     NativePanZoomController::LocalRef SetNativePanZoomController(NativePanZoomController::Param obj);
     void NotifyDefaultPrevented(uint64_t aInputBlockId, bool aDefaultPrevented);
 
 public: // GeckoContentController methods
     void RequestContentRepaint(const mozilla::layers::FrameMetrics& aFrameMetrics) MOZ_OVERRIDE;
+    void RequestFlingSnap(const mozilla::layers::FrameMetrics::ViewID& aScrollId,
+                          const mozilla::CSSPoint& aDestination) MOZ_OVERRIDE;
     void AcknowledgeScrollUpdate(const mozilla::layers::FrameMetrics::ViewID& aScrollId,
                                  const uint32_t& aScrollGeneration) MOZ_OVERRIDE;
     void HandleDoubleTap(const mozilla::CSSPoint& aPoint, Modifiers aModifiers,
                          const mozilla::layers::ScrollableLayerGuid& aGuid) MOZ_OVERRIDE;
     void HandleSingleTap(const mozilla::CSSPoint& aPoint, Modifiers aModifiers,
                          const mozilla::layers::ScrollableLayerGuid& aGuid) MOZ_OVERRIDE;
     void HandleLongTap(const mozilla::CSSPoint& aPoint, Modifiers aModifiers,
                        const mozilla::layers::ScrollableLayerGuid& aGuid,
--- a/widget/windows/winrt/APZController.cpp
+++ b/widget/windows/winrt/APZController.cpp
@@ -183,16 +183,27 @@ APZController::RequestContentRepaint(con
         metrics.GetDisplayPortMargins().right,
         metrics.GetDisplayPortMargins().bottom);
 #endif
     }
   }
 }
 
 void
+APZController::RequestFlingSnap(const FrameMetrics::ViewID& aScrollId,
+                                const mozilla::CSSPoint& aDestination)
+{
+#ifdef DEBUG_CONTROLLER
+  WinUtils::Log("APZController::RequestFlingSnap scrollid=%I64d destination: %lu %lu",
+    aScrollId, aDestination.x, aDestination.y);
+#endif
+  mozilla::layers::APZCCallbackHelper::RequestFlingSnap(aScrollId, aDestination);
+}
+
+void
 APZController::AcknowledgeScrollUpdate(const FrameMetrics::ViewID& aScrollId,
                                        const uint32_t& aScrollGeneration)
 {
 #ifdef DEBUG_CONTROLLER
   WinUtils::Log("APZController::AcknowledgeScrollUpdate scrollid=%I64d gen=%lu",
     aScrollId, aScrollGeneration);
 #endif
   mozilla::layers::APZCCallbackHelper::AcknowledgeScrollUpdate(aScrollId, aScrollGeneration);
--- a/widget/windows/winrt/APZController.h
+++ b/widget/windows/winrt/APZController.h
@@ -32,16 +32,18 @@ class APZController :
 public:
   APZController() :
     mFlusher(nullptr)
   {
   }
 
   // GeckoContentController interface
   virtual void RequestContentRepaint(const FrameMetrics& aFrameMetrics);
+  virtual void RequestFlingSnap(const FrameMetrics::ViewID& aScrollId,
+                                const mozilla::CSSPoint& aDestination);
   virtual void AcknowledgeScrollUpdate(const FrameMetrics::ViewID& aScrollId, const uint32_t& aScrollGeneration);
   virtual void HandleDoubleTap(const mozilla::CSSPoint& aPoint,
                                Modifiers aModifiers,
                                const mozilla::layers::ScrollableLayerGuid& aGuid);
   virtual void HandleSingleTap(const mozilla::CSSPoint& aPoint,
                                Modifiers aModifiers,
                                const mozilla::layers::ScrollableLayerGuid& aGuid);
   virtual void HandleLongTap(const mozilla::CSSPoint& aPoint,