Scroll snapping on Mac should use AppKit animations
authorwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 29 Nov 2016 16:10:55 +0000 (16:10 +0000)
committerwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 29 Nov 2016 16:10:55 +0000 (16:10 +0000)
https://bugs.webkit.org/show_bug.cgi?id=147261
<rdar://problem/29395293>

Reviewed by Brent Fulgham.

Source/WebCore:

Refactors the scroll snapping animation logic to support arbitrary scrolling momentum calculators and
introduces ScrollingMomentumCalculatorMac, which wraps AppKit's _NSScrollingMomentumCalculator. On macOS El
Capitan and later, we use the platform scrolling momentum calculator and for all other cases, we fall back to
the preexissting platform-invariant momentum calculator.

Previously, the scroll snapping animation logic was shared between the ScrollSnapAnimatorState and
ScrollController -- namely, the ScrollController would update various parameters of the ScrollSnapAnimatorState
and then tell it to compute animation-specific constants and coefficients. After this patch, ScrollController
will no longer directly set the ScrollSnapAnimatorState's member variables. Instead, it will tell the animator
state to transition to a new ScrollSnapState with the necessary parameters, and the ScrollSnapAnimatorState is
responsible for modifying itself accordingly. Furthermore, logic pertaining to computing animated scroll offsets
is now split out into a new ScrollingMomentumCalculator, which may have different platform-dependent
implementations. The correct calculator is initialized via ScrollingMomentumCalculator::create, which currently
returns a ScrollingMomentumCalculatorMac on El Capitan and later, and a BasicScrollingMomentumCalculator
otherwise.

The new abstracted ScrollingMomentumCalculator is initialized with various parameters describing the scrolled
content and viewport, as well as the initial and target scrolling offsets. The momentum calculator is then able
to compute the animated scroll offset at any given elapsed time, as well as the total duration of the snapping
animation. The ScrollController's scroll snap timer uses this information (via the ScrollSnapAnimatorState) to
animate its client's scroll offset during a snap or glide.

Also reenables 8 failing and/or flaky scroll snapping tests and adds a new layout test. This patch addresses
two causes for failures and flakiness in these scroll snapping tests:

1.  When starting or stopping the scroll snap animation timer, we call deferTestsForReason and
    removeTestDeferralForReason, respectively. These were actually noops for the first simulated scroll gesture
    on each of the failing mainframe scrolling tests due to m_expectsWheelEventTestTrigger being false. This
    member variable is updated when AsyncScrollingCoordinator::frameViewLayoutUpdated is invoked, wherein we
    call ScrollingStateFrameScrollingNode::setExpectsWheelEventTestTrigger(true) when the test has started
    monitoring wheel events. However, if this does not happen before scrolling begins in the test (which is the
    case here), then the mainframe scrolling node will not expect a wheel event test trigger even though
    eventSender.monitorWheelEvents() has been called. To fix this, we simply make the Page trigger a layout of
    the main FrameView when first ensuring the wheel event test trigger on the Page.

2.  The second reason for flakiness affects both overflow and mainframe scrolling. Previously, due to the way
    we would wait for multiple momentum scroll events before starting to glide, we would end up starting the
    scroll snap timer for a snapping animation, stopping it, and then starting it again for the glide animation.
    Thus, if the wheel event test trigger's timer fires right after the scroll snap timer stops and before it
    starts again due to a glide animation, it will erroneously think that scroll snapping is complete, even
    though it's only just about to begin! Now that we know scrolling velocity when we receive the initial
    "momentum begin", we now directly transition the scroll snap state from a snapping state to a gliding state
    and no longer stop and start the timer during this transition, which means that the test trigger will be
    deferred for at least the entire duration of the scroll snapping animation (starting right after the first
    "drag end" wheel event).

Test: tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-vertical-then-horizontal.html

* WebCore.xcodeproj/project.pbxproj:
* page/EventHandler.cpp:
(WebCore::handleWheelEventInAppropriateEnclosingBox):
(WebCore::EventHandler::defaultWheelEventHandler):
* page/Page.cpp:
(WebCore::Page::ensureTestTrigger):

Addresses test failures by forcing the mainframe scrolling node to expect wheel event test triggers.

* page/WheelEventDeltaFilter.cpp:
(WebCore::WheelEventDeltaFilter::create):
(WebCore::WheelEventDeltaFilter::filteredVelocity):
* page/WheelEventDeltaFilter.h:
* page/mac/WheelEventDeltaFilterMac.mm:
(WebCore::WheelEventDeltaFilterMac::updateFromDelta):

Add support for plumbing filtered scrolling velocity over to the ScrollController.

* page/scrolling/ScrollingMomentumCalculator.cpp: Copied from Source/WebCore/platform/cocoa/ScrollSnapAnimatorState.mm.
(WebCore::ScrollingMomentumCalculator::ScrollingMomentumCalculator):
(WebCore::ScrollingMomentumCalculator::create):

Creates a platform-independent BasicScrollingMomentumCalculator.

(WebCore::BasicScrollingMomentumCalculator::BasicScrollingMomentumCalculator):
(WebCore::BasicScrollingMomentumCalculator::linearlyInterpolatedOffsetAtProgress):
(WebCore::BasicScrollingMomentumCalculator::cubicallyInterpolatedOffsetAtProgress):
(WebCore::BasicScrollingMomentumCalculator::scrollOffsetAfterElapsedTime):
(WebCore::BasicScrollingMomentumCalculator::animationDuration):
(WebCore::BasicScrollingMomentumCalculator::initializeInterpolationCoefficientsIfNecessary):
(WebCore::BasicScrollingMomentumCalculator::initializeSnapProgressCurve):
(WebCore::BasicScrollingMomentumCalculator::animationProgressAfterElapsedTime):

Interpolation logic ported over from ScrollSnapAnimatorState.

* page/scrolling/ScrollingMomentumCalculator.h: Added.
(WebCore::ScrollingMomentumCalculator::~ScrollingMomentumCalculator):
* page/scrolling/mac/ScrollingMomentumCalculatorMac.h: Copied from Source/WebCore/page/WheelEventDeltaFilter.h.
* page/scrolling/mac/ScrollingMomentumCalculatorMac.mm: Added.
(WebCore::ScrollingMomentumCalculator::create):

Creates a ScrollingMomentumCalculatorMac.

(WebCore::ScrollingMomentumCalculatorMac::ScrollingMomentumCalculatorMac):
(WebCore::ScrollingMomentumCalculatorMac::scrollOffsetAfterElapsedTime):
(WebCore::ScrollingMomentumCalculatorMac::animationDuration):
(WebCore::ScrollingMomentumCalculatorMac::ensurePlatformMomentumCalculator):
* page/scrolling/mac/ScrollingTreeFrameScrollingNodeMac.h:
* page/scrolling/mac/ScrollingTreeFrameScrollingNodeMac.mm:
(WebCore::ScrollingTreeFrameScrollingNodeMac::scrollOffset):
(WebCore::ScrollingTreeFrameScrollingNodeMac::viewportSize):
(WebCore::ScrollingTreeFrameScrollingNodeMac::scrollOffsetOnAxis): Deleted.
* platform/PlatformWheelEvent.h:
(WebCore::PlatformWheelEvent::copyWithDeltasAndVelocity):
(WebCore::PlatformWheelEvent::scrollingVelocity):
(WebCore::PlatformWheelEvent::copyWithDeltas): Deleted.
* platform/ScrollAnimator.cpp:
(WebCore::ScrollAnimator::scrollOffset):
(WebCore::ScrollAnimator::viewportSize):
(WebCore::ScrollAnimator::scrollOffsetOnAxis): Deleted.
* platform/ScrollAnimator.h:
* platform/cocoa/ScrollController.h:
* platform/cocoa/ScrollController.mm:
(WebCore::otherScrollEventAxis):
(WebCore::ScrollController::ScrollController):
(WebCore::ScrollController::shouldOverrideInertialScrolling):
(WebCore::ScrollController::scheduleStatelessScrollSnap):
(WebCore::ScrollController::statelessSnapTransitionTimerFired):
(WebCore::ScrollController::startDeferringTestsDueToScrollSnapping):
(WebCore::ScrollController::stopDeferringTestsDueToScrollSnapping):
(WebCore::ScrollController::processWheelEventForScrollSnap):
(WebCore::ScrollController::updateScrollSnapState):
(WebCore::ScrollController::updateScrollSnapPoints):

Update the ScrollController's ScrollSnapAnimationState for both vertical and horizontal axes. If both axes lack
any snap points, the pointer to the animation state will be nulled out; otherwise, the animation state will
exist.

(WebCore::ScrollController::startScrollSnapTimer):
(WebCore::ScrollController::stopScrollSnapTimer):
(WebCore::ScrollController::scrollSnapTimerFired):
(WebCore::ScrollController::activeScrollSnapIndexForAxis):
(WebCore::ScrollController::setActiveScrollSnapIndexForAxis):
(WebCore::ScrollController::setNearestScrollSnapIndexForAxisAndOffset):
(WebCore::ScrollController::setActiveScrollSnapIndicesForOffset):
(WebCore::ScrollController::scrollSnapPointState): Deleted.
(WebCore::ScrollController::processWheelEventForScrollSnapOnAxis): Deleted.
(WebCore::ScrollController::shouldOverrideWheelEvent): Deleted.
(WebCore::projectedInertialScrollDistance): Deleted.
(WebCore::ScrollController::beginScrollSnapAnimation): Deleted.
(WebCore::ScrollController::endScrollSnapAnimation): Deleted.
(WebCore::ScrollController::initializeScrollSnapAnimationParameters): Deleted.
(WebCore::ScrollController::isSnappingOnAxis): Deleted.
* platform/cocoa/ScrollSnapAnimatorState.h:
(WebCore::ScrollSnapAnimatorState::snapOffsetsForAxis):
(WebCore::ScrollSnapAnimatorState::setSnapOffsetsForAxis):
(WebCore::ScrollSnapAnimatorState::currentState):
(WebCore::ScrollSnapAnimatorState::activeSnapIndexForAxis):
(WebCore::ScrollSnapAnimatorState::setActiveSnapIndexForAxis):
* platform/cocoa/ScrollSnapAnimatorState.mm:
(WebCore::projectedInertialScrollDistance):
(WebCore::ScrollSnapAnimatorState::transitionToSnapAnimationState):
(WebCore::ScrollSnapAnimatorState::transitionToGlideAnimationState):
(WebCore::ScrollSnapAnimatorState::transitionToUserInteractionState):
(WebCore::ScrollSnapAnimatorState::transitionToDestinationReachedState):

These methods are used to update the ScrollSnapAnimationState. These state transitions should (and do)
encapsulate all changes that need to be made to the animation state; in other words, the ScrollController should
no longer be reaching directly into the ScrollSnapAnimatorState to change member variables.

(WebCore::ScrollSnapAnimatorState::setupAnimationForState):
(WebCore::ScrollSnapAnimatorState::teardownAnimationForState):
(WebCore::ScrollSnapAnimatorState::currentAnimatedScrollOffset):
(WebCore::ScrollSnapAnimatorState::targetOffsetForStartOffset):
(WebCore::ScrollSnapAnimatorState::ScrollSnapAnimatorState): Deleted.
(WebCore::ScrollSnapAnimatorState::pushInitialWheelDelta): Deleted.
(WebCore::ScrollSnapAnimatorState::averageInitialWheelDelta): Deleted.
(WebCore::ScrollSnapAnimatorState::clearInitialWheelDeltaWindow): Deleted.
(WebCore::ScrollSnapAnimatorState::isSnapping): Deleted.
(WebCore::ScrollSnapAnimatorState::canReachTargetWithCurrentInitialScrollDelta): Deleted.
(WebCore::ScrollSnapAnimatorState::wheelDeltaTrackingIsInProgress): Deleted.
(WebCore::ScrollSnapAnimatorState::hasFinishedTrackingWheelDeltas): Deleted.
(WebCore::ScrollSnapAnimatorState::interpolatedOffsetAtProgress): Deleted.
(WebCore::ScrollSnapAnimationCurveState::initializeSnapProgressCurve): Deleted.
(WebCore::ScrollSnapAnimationCurveState::initializeInterpolationCoefficientsIfNecessary): Deleted.
(WebCore::ScrollSnapAnimationCurveState::interpolatedPositionAtProgress): Deleted.
(WebCore::ScrollSnapAnimationCurveState::shouldCompleteSnapAnimationImmediatelyAtTime): Deleted.
(WebCore::ScrollSnapAnimationCurveState::animationProgressAtTime): Deleted.

The ScrollSnapAnimatorState now tracks state across both axes. This simplifies coordinating scroll snapping in
both horizontal and vertical axes and fixes the issue of the scroll offset not snapping when performing a scroll
in one direction without momentum, then scrolling with momentum in the other direction in a single gesture.

* platform/spi/mac/NSScrollingMomentumCalculatorSPI.h: Added.

Source/WebKit2:

Add some logic to plumb filtered wheel velocity over to WebCore in the case of mainframe scrolling. See
WebCore/ChangeLog for more details.

* WebProcess/WebPage/EventDispatcher.cpp:
(WebKit::EventDispatcher::wheelEvent):

Source/WTF:

Introduce HAVE(NSSCROLLING_FILTERS), which is on for macOS El Capitan and later.

* wtf/Platform.h:

LayoutTests:

Fixes 8 previously failing scroll snapping tests in the tiled-drawing/scrolling/scroll-snap directory and
removes them from TestExpectations. Also adds a new layout test. See WebCore/ChangeLog for more details.

* platform/mac-wk2/TestExpectations:
* tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-2d-overflow-expected.txt:
* tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-borders-expected.txt:
* tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-slow-horizontal.html:
* tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-slow-vertical.html:
* tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-vertical-then-horizontal-expected.txt: Added.
* tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-vertical-then-horizontal.html: Added.
* tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-vertical.html:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@209070 268f45cc-cd09-0410-ab3c-d52691b4dbfc

34 files changed:
LayoutTests/ChangeLog
LayoutTests/platform/mac-wk2/TestExpectations
LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-2d-overflow-expected.txt
LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-borders-expected.txt
LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-slow-horizontal.html
LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-slow-vertical.html
LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-vertical-then-horizontal-expected.txt [new file with mode: 0644]
LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-vertical-then-horizontal.html [new file with mode: 0644]
LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-vertical.html
Source/WTF/ChangeLog
Source/WTF/wtf/Platform.h
Source/WebCore/ChangeLog
Source/WebCore/WebCore.xcodeproj/project.pbxproj
Source/WebCore/page/EventHandler.cpp
Source/WebCore/page/Page.cpp
Source/WebCore/page/WheelEventDeltaFilter.cpp
Source/WebCore/page/WheelEventDeltaFilter.h
Source/WebCore/page/mac/WheelEventDeltaFilterMac.mm
Source/WebCore/page/scrolling/ScrollingMomentumCalculator.cpp [new file with mode: 0644]
Source/WebCore/page/scrolling/ScrollingMomentumCalculator.h [new file with mode: 0644]
Source/WebCore/page/scrolling/mac/ScrollingMomentumCalculatorMac.h [new file with mode: 0644]
Source/WebCore/page/scrolling/mac/ScrollingMomentumCalculatorMac.mm [new file with mode: 0644]
Source/WebCore/page/scrolling/mac/ScrollingTreeFrameScrollingNodeMac.h
Source/WebCore/page/scrolling/mac/ScrollingTreeFrameScrollingNodeMac.mm
Source/WebCore/platform/PlatformWheelEvent.h
Source/WebCore/platform/ScrollAnimator.cpp
Source/WebCore/platform/ScrollAnimator.h
Source/WebCore/platform/cocoa/ScrollController.h
Source/WebCore/platform/cocoa/ScrollController.mm
Source/WebCore/platform/cocoa/ScrollSnapAnimatorState.h
Source/WebCore/platform/cocoa/ScrollSnapAnimatorState.mm
Source/WebCore/platform/spi/mac/NSScrollingMomentumCalculatorSPI.h [new file with mode: 0644]
Source/WebKit2/ChangeLog
Source/WebKit2/WebProcess/WebPage/EventDispatcher.cpp

index bcbed1a..c822b9c 100644 (file)
@@ -1,3 +1,23 @@
+2016-11-29  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        Scroll snapping on Mac should use AppKit animations
+        https://bugs.webkit.org/show_bug.cgi?id=147261
+        <rdar://problem/29395293>
+
+        Reviewed by Brent Fulgham.
+
+        Fixes 8 previously failing scroll snapping tests in the tiled-drawing/scrolling/scroll-snap directory and
+        removes them from TestExpectations. Also adds a new layout test. See WebCore/ChangeLog for more details.
+
+        * platform/mac-wk2/TestExpectations:
+        * tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-2d-overflow-expected.txt:
+        * tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-borders-expected.txt:
+        * tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-slow-horizontal.html:
+        * tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-slow-vertical.html:
+        * tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-vertical-then-horizontal-expected.txt: Added.
+        * tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-vertical-then-horizontal.html: Added.
+        * tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-vertical.html:
+
 2016-11-29  Brady Eidson  <beidson@apple.com>
 
         IndexedDB 2.0: Queue up completed requests in the client, handle them one by one.
index 78bdc95..b956940 100644 (file)
@@ -264,19 +264,9 @@ webkit.org/b/136554 tiled-drawing/scrolling/frames/frameset-nested-frame-scrolla
 webkit.org/b/139901 tiled-drawing/scrolling/frames/frameset-frame-scrollability.html [ Pass Failure ]
 
 webkit.org/b/162505 tiled-drawing/scrolling/latched-div-with-scroll-snap.html [ Pass Failure ]
-
-webkit.org/b/148405 tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-2d-overflow.html [ Pass Failure ]
-
-webkit.org/b/148407 tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-horizontal.html [ Pass Failure ]
 webkit.org/b/148407 tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-slow-horizontal.html [ Pass Failure ]
 webkit.org/b/148407 tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-slow-vertical.html [ Pass Failure ]
-webkit.org/b/148407 tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-vertical.html [ Pass Failure ]
 
-webkit.org/b/148408 tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-borders.html [ Pass Failure ]
-webkit.org/b/148408 tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-overflow.html [ Pass Failure ]
-webkit.org/b/148408 tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-rotated.html [ Pass Failure ]
-webkit.org/b/148408 tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-padding.html [ Pass Failure ]
-webkit.org/b/148408 tiled-drawing/scrolling/scroll-snap/scroll-snap-iframe.html [ Pass Failure ]
 webkit.org/b/148408 tiled-drawing/scrolling/root-overflow-with-mousewheel.html [ Pass Failure Timeout ]
 
 webkit.org/b/139820 fast/frames/lots-of-objects.html [ Timeout ]
index d1fe1ec..7a4752c 100644 (file)
@@ -1,6 +1,6 @@
 PASS div successfully scrolled diagonally.
 PASS div successfully snapped diagonally.
-FAIL div did not honor 2D snap points. (single axis scroll followed by flick on other axis)
+PASS div successfully snapped after dragging along one axis and then scrolling in the other.
 PASS successfullyParsed is true
 
 TEST COMPLETE
index 3b2a748..7a772aa 100644 (file)
@@ -4,9 +4,9 @@ PASS div scrolled to next window.
 Testing scroll-snap snap for horizontalTarget:
 PASS div honored snap points.
 Testing scroll-snap glide for verticalTarget:
-FAIL div did not honor snap points. Expected 300, but got 50
+PASS div scrolled to next window.
 Testing scroll-snap snap for verticalTarget:
-FAIL div did not snap back to proper location for verticalTarget. Expected 50, but got 0
+PASS div honored snap points.
 PASS successfullyParsed is true
 
 TEST COMPLETE
index c8ea29c..0159e30 100644 (file)
@@ -33,7 +33,7 @@
         function checkForScrollSnap() {
             // The div should have snapped back to the previous position
             if (divTarget.scrollLeft != divScrollPositionBeforeSnap)
-                testFailed("div did not snap back to proper location.");
+                testFailed(`div did not snap back to proper location. ${divTarget.scrollLeft} vs. ${divScrollPositionBeforeSnap}`);
             else
                 testPassed("div honored snap points.");
 
@@ -63,7 +63,7 @@
             if (divTarget.scrollLeft == window.innerWidth)
                 testPassed("div scrolled to next window.");
             else
-                testFailed("div did not honor snap points.");
+                testFailed(`div did not honor snap points. ${divTarget.scrollLeft} vs. ${window.innerWidth}`);
 
             setTimeout(scrollSnapTest, 0);
         }
index 3d49fbc..f287de7 100644 (file)
@@ -33,7 +33,7 @@
         function checkForScrollSnap() {
             // The div should have snapped back to the previous position
             if (divTarget.scrollTop != divScrollPositionBeforeSnap)
-                testFailed("div did not snap back to proper location.");
+                testFailed(`div did not snap back to proper location. (${divTarget.scrollTop} vs. ${divScrollPositionBeforeSnap})`);
             else
                 testPassed("div honored snap points.");
 
@@ -63,7 +63,7 @@
             if (divTarget.scrollTop == window.innerHeight)
                 testPassed("div scrolled to next window.");
             else
-                testFailed("div did not honor snap points.");
+                testFailed(`div did not honor snap points. (${divTarget.scrollTop} vs. ${window.innerHeight})`);
 
             setTimeout(scrollSnapTest, 0);
         }
@@ -88,7 +88,6 @@
         }
 
         function onLoad() {
-
             if (window.eventSender) {
                 eventSender.monitorWheelEvents();
                 setTimeout(scrollGlideTest, 0);
diff --git a/LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-vertical-then-horizontal-expected.txt b/LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-vertical-then-horizontal-expected.txt
new file mode 100644 (file)
index 0000000..4a9c9b8
--- /dev/null
@@ -0,0 +1,5 @@
+PASS scroll offset snapped back to top.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-vertical-then-horizontal.html b/LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-vertical-then-horizontal.html
new file mode 100644 (file)
index 0000000..3974bf3
--- /dev/null
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+    <head>
+        <style>
+            .verticalGallery {
+                width: 100vw;
+                height: 600vh;
+                margin: 0;
+                padding: 0;
+                -webkit-scroll-snap-points-y: repeat(100vh);
+                -webkit-scroll-snap-type: mandatory;
+            }
+            .colorBox {
+                height: 100vh;
+                width: 100vw;
+                float: left;
+            }
+            #item0 { background-color: red; }
+            #item1 { background-color: green; }
+            #item2 { background-color: blue; }
+            #item3 { background-color: aqua; }
+            #item4 { background-color: yellow; }
+            #item5 { background-color: fuchsia; }
+        </style>
+        <script src="../../../resources/js-test.js"></script>
+        <script>
+        window.jsTestIsAsync = true;
+
+        function scrollSnapTest() {
+            var startPosX = document.body.offsetLeft + 20;
+            var startPosY = document.body.offsetTop + 20;
+            eventSender.mouseMoveTo(startPosX, startPosY);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(0, -1, 'began', 'none');
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(0, -1, 'changed', 'none');
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(-1, 0, 'changed', 'none');
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(-1, 0, 'changed', 'none');
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(-1, 0, 'changed', 'none');
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(0, 0, 'ended', 'none');
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(-1, 0, 'none', 'begin');
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(-4, 0, 'none', 'continue');
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(0, 0, 'none', 'end');
+            eventSender.callAfterScrollingCompletes(() => {
+                if (document.body.scrollTop == 0)
+                    testPassed("scroll offset snapped back to top.");
+                else
+                    testFailed(`did not honor snap points (expected 0, observed ${document.body.scrollTop}).`);
+
+                finishJSTest();
+            });
+        }
+
+        function onLoad() {
+            if (window.eventSender) {
+                eventSender.monitorWheelEvents();
+                setTimeout(scrollSnapTest, 0);
+            } else {
+                var messageLocation = document.getElementById('item0');
+                var message = document.createElement('div');
+                message.innerHTML = `<p>This test is better run under DumpRenderTree. To manually test it, scroll down
+                slightly, and then directly to the left or right with momentum without lifting your fingers from the
+                trackpad. The scroll offset should animate to the nearest snap offset.</p>`;
+                messageLocation.appendChild(message);
+            }
+        }
+        </script>
+    </head>
+    <body onload="onLoad();" class="verticalGallery">
+        <div id="item0" class="colorBox"><div id="console"></div></div>
+        <div id="item1" class="colorBox"></div>
+        <div id="item2" class="colorBox"></div>
+        <div id="item3" class="colorBox"></div>
+        <div id="item4" class="colorBox"></div>
+        <div id="item5" class="colorBox"></div>
+    </body>
+</html>
\ No newline at end of file
index eeb82d0..8724b5b 100644 (file)
@@ -33,7 +33,7 @@
         function checkForScrollSnap() {
             // The div should have snapped back to the previous position
             if (divTarget.scrollTop != divScrollPositionBeforeSnap)
-                testFailed("div did not snap back to proper location.");
+                testFailed(`div did not snap back to proper location. (${divTarget.scrollTop} vs. ${divScrollPositionBeforeSnap})`);
             else
                 testPassed("div honored snap points.");
 
@@ -63,7 +63,7 @@
             if (divTarget.scrollTop == window.innerHeight)
                 testPassed("div scrolled to next window.");
             else
-                testFailed("div did not honor snap points.");
+                testFailed(`div did not honor snap points. (${divTarget.scrollTop} vs. ${window.innerHeight})`);
 
             setTimeout(scrollSnapTest, 0);
         }
index 0b1a24f..f19f6d7 100644 (file)
@@ -1,3 +1,15 @@
+2016-11-29  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        Scroll snapping on Mac should use AppKit animations
+        https://bugs.webkit.org/show_bug.cgi?id=147261
+        <rdar://problem/29395293>
+
+        Reviewed by Brent Fulgham.
+
+        Introduce HAVE(NSSCROLLING_FILTERS), which is on for macOS El Capitan and later.
+
+        * wtf/Platform.h:
+
 2016-11-28  Darin Adler  <darin@apple.com>
 
         Streamline and speed up tokenizer and segmented string classes
index 0f67831..ae3c826 100644 (file)
 #define USE_PLUGIN_HOST_PROCESS 1
 #endif
 
+#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 101100
+#define HAVE_NSSCROLLING_FILTERS 1
+#else
+#define HAVE_NSSCROLLING_FILTERS 0
+#endif
+
 /* OS X defines a series of platform macros for debugging. */
 /* Some of them are really annoying because they use common names (e.g. check()). */
 /* Disable those macros so that we are not limited in how we name methods and functions. */
index 119ef51..cda71a2 100644 (file)
@@ -1,3 +1,194 @@
+2016-11-29  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        Scroll snapping on Mac should use AppKit animations
+        https://bugs.webkit.org/show_bug.cgi?id=147261
+        <rdar://problem/29395293>
+
+        Reviewed by Brent Fulgham.
+
+        Refactors the scroll snapping animation logic to support arbitrary scrolling momentum calculators and
+        introduces ScrollingMomentumCalculatorMac, which wraps AppKit's _NSScrollingMomentumCalculator. On macOS El
+        Capitan and later, we use the platform scrolling momentum calculator and for all other cases, we fall back to
+        the preexissting platform-invariant momentum calculator.
+
+        Previously, the scroll snapping animation logic was shared between the ScrollSnapAnimatorState and
+        ScrollController -- namely, the ScrollController would update various parameters of the ScrollSnapAnimatorState
+        and then tell it to compute animation-specific constants and coefficients. After this patch, ScrollController
+        will no longer directly set the ScrollSnapAnimatorState's member variables. Instead, it will tell the animator
+        state to transition to a new ScrollSnapState with the necessary parameters, and the ScrollSnapAnimatorState is
+        responsible for modifying itself accordingly. Furthermore, logic pertaining to computing animated scroll offsets
+        is now split out into a new ScrollingMomentumCalculator, which may have different platform-dependent
+        implementations. The correct calculator is initialized via ScrollingMomentumCalculator::create, which currently
+        returns a ScrollingMomentumCalculatorMac on El Capitan and later, and a BasicScrollingMomentumCalculator
+        otherwise.
+
+        The new abstracted ScrollingMomentumCalculator is initialized with various parameters describing the scrolled
+        content and viewport, as well as the initial and target scrolling offsets. The momentum calculator is then able
+        to compute the animated scroll offset at any given elapsed time, as well as the total duration of the snapping
+        animation. The ScrollController's scroll snap timer uses this information (via the ScrollSnapAnimatorState) to
+        animate its client's scroll offset during a snap or glide.
+
+        Also reenables 8 failing and/or flaky scroll snapping tests and adds a new layout test. This patch addresses
+        two causes for failures and flakiness in these scroll snapping tests:
+
+        1.  When starting or stopping the scroll snap animation timer, we call deferTestsForReason and
+            removeTestDeferralForReason, respectively. These were actually noops for the first simulated scroll gesture
+            on each of the failing mainframe scrolling tests due to m_expectsWheelEventTestTrigger being false. This
+            member variable is updated when AsyncScrollingCoordinator::frameViewLayoutUpdated is invoked, wherein we
+            call ScrollingStateFrameScrollingNode::setExpectsWheelEventTestTrigger(true) when the test has started
+            monitoring wheel events. However, if this does not happen before scrolling begins in the test (which is the
+            case here), then the mainframe scrolling node will not expect a wheel event test trigger even though
+            eventSender.monitorWheelEvents() has been called. To fix this, we simply make the Page trigger a layout of
+            the main FrameView when first ensuring the wheel event test trigger on the Page.
+
+        2.  The second reason for flakiness affects both overflow and mainframe scrolling. Previously, due to the way
+            we would wait for multiple momentum scroll events before starting to glide, we would end up starting the
+            scroll snap timer for a snapping animation, stopping it, and then starting it again for the glide animation.
+            Thus, if the wheel event test trigger's timer fires right after the scroll snap timer stops and before it
+            starts again due to a glide animation, it will erroneously think that scroll snapping is complete, even
+            though it's only just about to begin! Now that we know scrolling velocity when we receive the initial
+            "momentum begin", we now directly transition the scroll snap state from a snapping state to a gliding state
+            and no longer stop and start the timer during this transition, which means that the test trigger will be
+            deferred for at least the entire duration of the scroll snapping animation (starting right after the first
+            "drag end" wheel event).
+
+        Test: tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-vertical-then-horizontal.html
+
+        * WebCore.xcodeproj/project.pbxproj:
+        * page/EventHandler.cpp:
+        (WebCore::handleWheelEventInAppropriateEnclosingBox):
+        (WebCore::EventHandler::defaultWheelEventHandler):
+        * page/Page.cpp:
+        (WebCore::Page::ensureTestTrigger):
+
+        Addresses test failures by forcing the mainframe scrolling node to expect wheel event test triggers.
+
+        * page/WheelEventDeltaFilter.cpp:
+        (WebCore::WheelEventDeltaFilter::create):
+        (WebCore::WheelEventDeltaFilter::filteredVelocity):
+        * page/WheelEventDeltaFilter.h:
+        * page/mac/WheelEventDeltaFilterMac.mm:
+        (WebCore::WheelEventDeltaFilterMac::updateFromDelta):
+
+        Add support for plumbing filtered scrolling velocity over to the ScrollController.
+
+        * page/scrolling/ScrollingMomentumCalculator.cpp: Copied from Source/WebCore/platform/cocoa/ScrollSnapAnimatorState.mm.
+        (WebCore::ScrollingMomentumCalculator::ScrollingMomentumCalculator):
+        (WebCore::ScrollingMomentumCalculator::create):
+
+        Creates a platform-independent BasicScrollingMomentumCalculator.
+
+        (WebCore::BasicScrollingMomentumCalculator::BasicScrollingMomentumCalculator):
+        (WebCore::BasicScrollingMomentumCalculator::linearlyInterpolatedOffsetAtProgress):
+        (WebCore::BasicScrollingMomentumCalculator::cubicallyInterpolatedOffsetAtProgress):
+        (WebCore::BasicScrollingMomentumCalculator::scrollOffsetAfterElapsedTime):
+        (WebCore::BasicScrollingMomentumCalculator::animationDuration):
+        (WebCore::BasicScrollingMomentumCalculator::initializeInterpolationCoefficientsIfNecessary):
+        (WebCore::BasicScrollingMomentumCalculator::initializeSnapProgressCurve):
+        (WebCore::BasicScrollingMomentumCalculator::animationProgressAfterElapsedTime):
+
+        Interpolation logic ported over from ScrollSnapAnimatorState.
+
+        * page/scrolling/ScrollingMomentumCalculator.h: Added.
+        (WebCore::ScrollingMomentumCalculator::~ScrollingMomentumCalculator):
+        * page/scrolling/mac/ScrollingMomentumCalculatorMac.h: Copied from Source/WebCore/page/WheelEventDeltaFilter.h.
+        * page/scrolling/mac/ScrollingMomentumCalculatorMac.mm: Added.
+        (WebCore::ScrollingMomentumCalculator::create):
+
+        Creates a ScrollingMomentumCalculatorMac.
+
+        (WebCore::ScrollingMomentumCalculatorMac::ScrollingMomentumCalculatorMac):
+        (WebCore::ScrollingMomentumCalculatorMac::scrollOffsetAfterElapsedTime):
+        (WebCore::ScrollingMomentumCalculatorMac::animationDuration):
+        (WebCore::ScrollingMomentumCalculatorMac::ensurePlatformMomentumCalculator):
+        * page/scrolling/mac/ScrollingTreeFrameScrollingNodeMac.h:
+        * page/scrolling/mac/ScrollingTreeFrameScrollingNodeMac.mm:
+        (WebCore::ScrollingTreeFrameScrollingNodeMac::scrollOffset):
+        (WebCore::ScrollingTreeFrameScrollingNodeMac::viewportSize):
+        (WebCore::ScrollingTreeFrameScrollingNodeMac::scrollOffsetOnAxis): Deleted.
+        * platform/PlatformWheelEvent.h:
+        (WebCore::PlatformWheelEvent::copyWithDeltasAndVelocity):
+        (WebCore::PlatformWheelEvent::scrollingVelocity):
+        (WebCore::PlatformWheelEvent::copyWithDeltas): Deleted.
+        * platform/ScrollAnimator.cpp:
+        (WebCore::ScrollAnimator::scrollOffset):
+        (WebCore::ScrollAnimator::viewportSize):
+        (WebCore::ScrollAnimator::scrollOffsetOnAxis): Deleted.
+        * platform/ScrollAnimator.h:
+        * platform/cocoa/ScrollController.h:
+        * platform/cocoa/ScrollController.mm:
+        (WebCore::otherScrollEventAxis):
+        (WebCore::ScrollController::ScrollController):
+        (WebCore::ScrollController::shouldOverrideInertialScrolling):
+        (WebCore::ScrollController::scheduleStatelessScrollSnap):
+        (WebCore::ScrollController::statelessSnapTransitionTimerFired):
+        (WebCore::ScrollController::startDeferringTestsDueToScrollSnapping):
+        (WebCore::ScrollController::stopDeferringTestsDueToScrollSnapping):
+        (WebCore::ScrollController::processWheelEventForScrollSnap):
+        (WebCore::ScrollController::updateScrollSnapState):
+        (WebCore::ScrollController::updateScrollSnapPoints):
+
+        Update the ScrollController's ScrollSnapAnimationState for both vertical and horizontal axes. If both axes lack
+        any snap points, the pointer to the animation state will be nulled out; otherwise, the animation state will
+        exist.
+
+        (WebCore::ScrollController::startScrollSnapTimer):
+        (WebCore::ScrollController::stopScrollSnapTimer):
+        (WebCore::ScrollController::scrollSnapTimerFired):
+        (WebCore::ScrollController::activeScrollSnapIndexForAxis):
+        (WebCore::ScrollController::setActiveScrollSnapIndexForAxis):
+        (WebCore::ScrollController::setNearestScrollSnapIndexForAxisAndOffset):
+        (WebCore::ScrollController::setActiveScrollSnapIndicesForOffset):
+        (WebCore::ScrollController::scrollSnapPointState): Deleted.
+        (WebCore::ScrollController::processWheelEventForScrollSnapOnAxis): Deleted.
+        (WebCore::ScrollController::shouldOverrideWheelEvent): Deleted.
+        (WebCore::projectedInertialScrollDistance): Deleted.
+        (WebCore::ScrollController::beginScrollSnapAnimation): Deleted.
+        (WebCore::ScrollController::endScrollSnapAnimation): Deleted.
+        (WebCore::ScrollController::initializeScrollSnapAnimationParameters): Deleted.
+        (WebCore::ScrollController::isSnappingOnAxis): Deleted.
+        * platform/cocoa/ScrollSnapAnimatorState.h:
+        (WebCore::ScrollSnapAnimatorState::snapOffsetsForAxis):
+        (WebCore::ScrollSnapAnimatorState::setSnapOffsetsForAxis):
+        (WebCore::ScrollSnapAnimatorState::currentState):
+        (WebCore::ScrollSnapAnimatorState::activeSnapIndexForAxis):
+        (WebCore::ScrollSnapAnimatorState::setActiveSnapIndexForAxis):
+        * platform/cocoa/ScrollSnapAnimatorState.mm:
+        (WebCore::projectedInertialScrollDistance):
+        (WebCore::ScrollSnapAnimatorState::transitionToSnapAnimationState):
+        (WebCore::ScrollSnapAnimatorState::transitionToGlideAnimationState):
+        (WebCore::ScrollSnapAnimatorState::transitionToUserInteractionState):
+        (WebCore::ScrollSnapAnimatorState::transitionToDestinationReachedState):
+
+        These methods are used to update the ScrollSnapAnimationState. These state transitions should (and do)
+        encapsulate all changes that need to be made to the animation state; in other words, the ScrollController should
+        no longer be reaching directly into the ScrollSnapAnimatorState to change member variables.
+
+        (WebCore::ScrollSnapAnimatorState::setupAnimationForState):
+        (WebCore::ScrollSnapAnimatorState::teardownAnimationForState):
+        (WebCore::ScrollSnapAnimatorState::currentAnimatedScrollOffset):
+        (WebCore::ScrollSnapAnimatorState::targetOffsetForStartOffset):
+        (WebCore::ScrollSnapAnimatorState::ScrollSnapAnimatorState): Deleted.
+        (WebCore::ScrollSnapAnimatorState::pushInitialWheelDelta): Deleted.
+        (WebCore::ScrollSnapAnimatorState::averageInitialWheelDelta): Deleted.
+        (WebCore::ScrollSnapAnimatorState::clearInitialWheelDeltaWindow): Deleted.
+        (WebCore::ScrollSnapAnimatorState::isSnapping): Deleted.
+        (WebCore::ScrollSnapAnimatorState::canReachTargetWithCurrentInitialScrollDelta): Deleted.
+        (WebCore::ScrollSnapAnimatorState::wheelDeltaTrackingIsInProgress): Deleted.
+        (WebCore::ScrollSnapAnimatorState::hasFinishedTrackingWheelDeltas): Deleted.
+        (WebCore::ScrollSnapAnimatorState::interpolatedOffsetAtProgress): Deleted.
+        (WebCore::ScrollSnapAnimationCurveState::initializeSnapProgressCurve): Deleted.
+        (WebCore::ScrollSnapAnimationCurveState::initializeInterpolationCoefficientsIfNecessary): Deleted.
+        (WebCore::ScrollSnapAnimationCurveState::interpolatedPositionAtProgress): Deleted.
+        (WebCore::ScrollSnapAnimationCurveState::shouldCompleteSnapAnimationImmediatelyAtTime): Deleted.
+        (WebCore::ScrollSnapAnimationCurveState::animationProgressAtTime): Deleted.
+
+        The ScrollSnapAnimatorState now tracks state across both axes. This simplifies coordinating scroll snapping in
+        both horizontal and vertical axes and fixes the issue of the scroll offset not snapping when performing a scroll
+        in one direction without momentum, then scrolling with momentum in the other direction in a single gesture.
+
+        * platform/spi/mac/NSScrollingMomentumCalculatorSPI.h: Added.
+
 2016-11-29  Brady Eidson  <beidson@apple.com>
 
         IndexedDB 2.0: Queue up completed requests in the client, handle them one by one.
index e376ece..281303e 100644 (file)
                517A63C61B74319200E7DCDC /* KeyedEncoderCF.h in Headers */ = {isa = PBXBuildFile; fileRef = 517A63C21B74317E00E7DCDC /* KeyedEncoderCF.h */; settings = {ATTRIBUTES = (Private, ); }; };
                517B25A91CC82B2A0061C011 /* IDBConnectionProxy.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 517B25A71CC820320061C011 /* IDBConnectionProxy.cpp */; };
                517B25AA1CC82B2A0061C011 /* IDBConnectionProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 517B25A81CC820320061C011 /* IDBConnectionProxy.h */; settings = {ATTRIBUTES = (Private, ); }; };
+               517DEEE51DE94ADC00B91644 /* ScrollingMomentumCalculatorMac.mm in Sources */ = {isa = PBXBuildFile; fileRef = 517DEEE31DE94ADC00B91644 /* ScrollingMomentumCalculatorMac.mm */; };
+               517DEEE81DE94B0800B91644 /* ScrollingMomentumCalculatorMac.h in Headers */ = {isa = PBXBuildFile; fileRef = 517DEEE71DE94B0800B91644 /* ScrollingMomentumCalculatorMac.h */; settings = {ATTRIBUTES = (Private, ); }; };
                517FBA1E151AB17C00B57959 /* DOMWindowExtension.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 517FBA17151AA71B00B57959 /* DOMWindowExtension.cpp */; };
                5185FC741BB4C4E80012898F /* DOMWindowIndexedDatabase.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 51D7196C181106DF0016DC51 /* DOMWindowIndexedDatabase.cpp */; };
                5185FC751BB4C4E80012898F /* DOMWindowIndexedDatabase.h in Headers */ = {isa = PBXBuildFile; fileRef = 51D7196D181106DF0016DC51 /* DOMWindowIndexedDatabase.h */; };
                51BE37E00DAEE00E001085FC /* StorageArea.h in Headers */ = {isa = PBXBuildFile; fileRef = 51BE37DE0DAEE00E001085FC /* StorageArea.h */; settings = {ATTRIBUTES = (Private, ); }; };
                51C0AA390F2AA10A001648C2 /* CachedFrame.h in Headers */ = {isa = PBXBuildFile; fileRef = 51C0AA380F2AA10A001648C2 /* CachedFrame.h */; settings = {ATTRIBUTES = (Private, ); }; };
                51C0AA410F2AA15E001648C2 /* CachedFrame.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 51C0AA400F2AA15E001648C2 /* CachedFrame.cpp */; };
+               51C61B0A1DE536E7008A212D /* ScrollingMomentumCalculator.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 51C61B081DE536E7008A212D /* ScrollingMomentumCalculator.cpp */; };
+               51C61B0B1DE536E7008A212D /* ScrollingMomentumCalculator.h in Headers */ = {isa = PBXBuildFile; fileRef = 51C61B091DE536E7008A212D /* ScrollingMomentumCalculator.h */; settings = {ATTRIBUTES = (Private, ); }; };
                51C81B890C4422F70019ECE3 /* FTPDirectoryParser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 51C81B870C4422F70019ECE3 /* FTPDirectoryParser.cpp */; };
                51C81B8A0C4422F70019ECE3 /* FTPDirectoryParser.h in Headers */ = {isa = PBXBuildFile; fileRef = 51C81B880C4422F70019ECE3 /* FTPDirectoryParser.h */; };
                51CBFC990D10E483002DBF51 /* CachedFramePlatformData.h in Headers */ = {isa = PBXBuildFile; fileRef = 51CBFC980D10E483002DBF51 /* CachedFramePlatformData.h */; settings = {ATTRIBUTES = (Private, ); }; };
                517A63C21B74317E00E7DCDC /* KeyedEncoderCF.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KeyedEncoderCF.h; sourceTree = "<group>"; };
                517B25A71CC820320061C011 /* IDBConnectionProxy.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = IDBConnectionProxy.cpp; sourceTree = "<group>"; };
                517B25A81CC820320061C011 /* IDBConnectionProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IDBConnectionProxy.h; sourceTree = "<group>"; };
+               517DEEE31DE94ADC00B91644 /* ScrollingMomentumCalculatorMac.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ScrollingMomentumCalculatorMac.mm; sourceTree = "<group>"; };
+               517DEEE71DE94B0800B91644 /* ScrollingMomentumCalculatorMac.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ScrollingMomentumCalculatorMac.h; sourceTree = "<group>"; };
                517FBA17151AA71B00B57959 /* DOMWindowExtension.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = DOMWindowExtension.cpp; sourceTree = "<group>"; };
                517FBA18151AA71B00B57959 /* DOMWindowExtension.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DOMWindowExtension.h; sourceTree = "<group>"; };
                5185FCBC1BB5CB770012898F /* IDBConnectionToServer.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = IDBConnectionToServer.cpp; sourceTree = "<group>"; };
                51BE37DE0DAEE00E001085FC /* StorageArea.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StorageArea.h; sourceTree = "<group>"; };
                51C0AA380F2AA10A001648C2 /* CachedFrame.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CachedFrame.h; sourceTree = "<group>"; };
                51C0AA400F2AA15E001648C2 /* CachedFrame.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CachedFrame.cpp; sourceTree = "<group>"; };
+               51C61B081DE536E7008A212D /* ScrollingMomentumCalculator.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ScrollingMomentumCalculator.cpp; sourceTree = "<group>"; };
+               51C61B091DE536E7008A212D /* ScrollingMomentumCalculator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ScrollingMomentumCalculator.h; sourceTree = "<group>"; };
+               51C61B0C1DE5383D008A212D /* NSScrollingMomentumCalculatorSPI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSScrollingMomentumCalculatorSPI.h; sourceTree = "<group>"; };
                51C81B870C4422F70019ECE3 /* FTPDirectoryParser.cpp */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.cpp.cpp; path = FTPDirectoryParser.cpp; sourceTree = "<group>"; };
                51C81B880C4422F70019ECE3 /* FTPDirectoryParser.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = FTPDirectoryParser.h; sourceTree = "<group>"; };
                51CBFC980D10E483002DBF51 /* CachedFramePlatformData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CachedFramePlatformData.h; sourceTree = "<group>"; };
                                0FFD4D5F18651FA300512F6E /* AsyncScrollingCoordinator.h */,
                                F45C231B1995B73B00A6E2E3 /* AxisScrollSnapOffsets.cpp */,
                                F45C231C1995B73B00A6E2E3 /* AxisScrollSnapOffsets.h */,
+                               51C61B081DE536E7008A212D /* ScrollingMomentumCalculator.cpp */,
+                               51C61B091DE536E7008A212D /* ScrollingMomentumCalculator.h */,
                                0F605AEA15F94848004DF0C0 /* ScrollingConstraints.cpp */,
                                0F605AEB15F94848004DF0C0 /* ScrollingConstraints.h */,
                                1AF62EE414DA22A70041556C /* ScrollingCoordinator.cpp */,
                1AF62EE214DA22A70041556C /* mac */ = {
                        isa = PBXGroup;
                        children = (
+                               517DEEE71DE94B0800B91644 /* ScrollingMomentumCalculatorMac.h */,
+                               517DEEE31DE94ADC00B91644 /* ScrollingMomentumCalculatorMac.mm */,
                                9391A990162746CB00297330 /* ScrollingCoordinatorMac.h */,
                                1AF62EE314DA22A70041556C /* ScrollingCoordinatorMac.mm */,
                                0FA88EBC16A8D1BD00F99984 /* ScrollingStateFrameScrollingNodeMac.mm */,
                                93F1E1EB1A40FDDC00348D13 /* NSPopoverSPI.h */,
                                7C0406121C66EE9C00AF0711 /* NSScrollerImpSPI.h */,
                                F40EA8AA1B867D6500CE5581 /* NSScrollingInputFilterSPI.h */,
+                               51C61B0C1DE5383D008A212D /* NSScrollingMomentumCalculatorSPI.h */,
                                2DCB837719F99BBA00A7FBE4 /* NSSharingServicePickerSPI.h */,
                                2DCB837819F99BBA00A7FBE4 /* NSSharingServiceSPI.h */,
                                933C7A741C0FBC440034FB97 /* NSSpellCheckerSPI.h */,
                                5126E6BC0A2E3B12005C29FA /* IconDatabase.h in Headers */,
                                516953981329A3C800B92D04 /* IconDatabaseBase.h in Headers */,
                                51E1ECBE0C91C90400DC255B /* IconDatabaseClient.h in Headers */,
+                               517DEEE81DE94B0800B91644 /* ScrollingMomentumCalculatorMac.h in Headers */,
                                513F14540AB634C400094DDF /* IconLoader.h in Headers */,
                                51E1ECC10C91C90400DC255B /* IconRecord.h in Headers */,
                                51714EA81CF4E4B1004723C4 /* IDBActiveDOMObject.h in Headers */,
                                9BDA64D81B975CF2009C4387 /* JSShadowRoot.h in Headers */,
                                46DFF49C1DC2620B00B80B48 /* JSShadowRootMode.h in Headers */,
                                CD9DE17B17AAC75B00EA386D /* JSSourceBuffer.h in Headers */,
+                               51C61B0B1DE536E7008A212D /* ScrollingMomentumCalculator.h in Headers */,
                                CD9DE17D17AAC75B00EA386D /* JSSourceBufferList.h in Headers */,
                                AA7FEEAD16A4E74B004C0C33 /* JSSpeechSynthesis.h in Headers */,
                                AA2A5AD216A4860A00976A25 /* JSSpeechSynthesisEvent.h in Headers */,
                                46C83EFD1A9BBE2900A79A41 /* GeoNotifier.cpp in Sources */,
                                2D5036681BCDDDC400E20BB3 /* GestureEvents.cpp in Sources */,
                                B2AFFC830D00A5C10030074D /* GlyphPageMac.cpp in Sources */,
+                               51C61B0A1DE536E7008A212D /* ScrollingMomentumCalculator.cpp in Sources */,
                                BC53C6080DA56C570021EB5D /* Gradient.cpp in Sources */,
                                BC53C60B0DA56CF10021EB5D /* GradientCG.cpp in Sources */,
                                2D481F03146B5C6500AA7834 /* GradientImage.cpp in Sources */,
                                AB67D1A8097F3AE300F9392E /* RenderTextControl.cpp in Sources */,
                                083DAEA60F01A7FB00342754 /* RenderTextControlMultiLine.cpp in Sources */,
                                083DAEA80F01A7FB00342754 /* RenderTextControlSingleLine.cpp in Sources */,
+                               517DEEE51DE94ADC00B91644 /* ScrollingMomentumCalculatorMac.mm in Sources */,
                                BCEA488D097D93020094C9E4 /* RenderTextFragment.cpp in Sources */,
                                E4C91A18180999FB00A17F6D /* RenderTextLineBoxes.cpp in Sources */,
                                BCEA4889097D93020094C9E4 /* RenderTheme.cpp in Sources */,
index e94a925..4e8772c 100644 (file)
@@ -303,7 +303,7 @@ static inline bool didScrollInScrollableArea(ScrollableArea* scrollableArea, Whe
     return didHandleWheelEvent;
 }
 
-static inline bool handleWheelEventInAppropriateEnclosingBox(Node* startNode, WheelEvent& wheelEvent, Element** stopElement, const FloatSize& filteredPlatformDelta)
+static inline bool handleWheelEventInAppropriateEnclosingBox(Node* startNode, WheelEvent& wheelEvent, Element** stopElement, const FloatSize& filteredPlatformDelta, const FloatPoint& filteredVelocity)
 {
     bool shouldHandleEvent = wheelEvent.deltaX() || wheelEvent.deltaY();
 #if PLATFORM(MAC)
@@ -324,9 +324,10 @@ static inline bool handleWheelEventInAppropriateEnclosingBox(Node* startNode, Wh
         if (RenderLayer* boxLayer = currentEnclosingBox->layer()) {
             const PlatformWheelEvent* platformEvent = wheelEvent.wheelEvent();
             bool scrollingWasHandled;
-            if (platformEvent != nullptr)
-                scrollingWasHandled = boxLayer->handleWheelEvent(platformEvent->copyWithDeltas(filteredPlatformDelta.width(), filteredPlatformDelta.height()));
-            else
+            if (platformEvent != nullptr) {
+                auto copiedEvent = platformEvent->copyWithDeltasAndVelocity(filteredPlatformDelta.width(), filteredPlatformDelta.height(), filteredVelocity);
+                scrollingWasHandled = boxLayer->handleWheelEvent(copiedEvent);
+            } else
                 scrollingWasHandled = didScrollInScrollableArea(boxLayer, wheelEvent);
 
             if (scrollingWasHandled) {
@@ -2757,6 +2758,7 @@ void EventHandler::defaultWheelEventHandler(Node* startNode, WheelEvent& wheelEv
     Ref<Frame> protectedFrame(m_frame);
 
     FloatSize filteredPlatformDelta(wheelEvent.deltaX(), wheelEvent.deltaY());
+    FloatPoint filteredVelocity;
     if (const PlatformWheelEvent* platformWheelEvent = wheelEvent.wheelEvent()) {
         filteredPlatformDelta.setWidth(platformWheelEvent->deltaX());
         filteredPlatformDelta.setHeight(platformWheelEvent->deltaY());
@@ -2766,14 +2768,16 @@ void EventHandler::defaultWheelEventHandler(Node* startNode, WheelEvent& wheelEv
     ScrollLatchingState* latchedState = m_frame.mainFrame().latchingState();
     Element* stopElement = latchedState ? latchedState->previousWheelScrolledElement() : nullptr;
 
-    if (m_frame.mainFrame().wheelEventDeltaFilter()->isFilteringDeltas())
+    if (m_frame.mainFrame().wheelEventDeltaFilter()->isFilteringDeltas()) {
         filteredPlatformDelta = m_frame.mainFrame().wheelEventDeltaFilter()->filteredDelta();
+        filteredVelocity = m_frame.mainFrame().wheelEventDeltaFilter()->filteredVelocity();
+    }
 #else
     Element* stopElement = nullptr;
 #endif
     
     
-    if (handleWheelEventInAppropriateEnclosingBox(startNode, wheelEvent, &stopElement, filteredPlatformDelta))
+    if (handleWheelEventInAppropriateEnclosingBox(startNode, wheelEvent, &stopElement, filteredPlatformDelta, filteredVelocity))
         wheelEvent.setDefaultHandled();
     
 #if PLATFORM(MAC)
index 5fe21e7..e6c31b4 100644 (file)
@@ -1993,8 +1993,11 @@ void Page::setShouldPlayToPlaybackTarget(uint64_t clientId, bool shouldPlay)
 
 WheelEventTestTrigger& Page::ensureTestTrigger()
 {
-    if (!m_testTrigger)
+    if (!m_testTrigger) {
         m_testTrigger = adoptRef(new WheelEventTestTrigger());
+        if (auto* frameView = mainFrame().view())
+            frameView->layout();
+    }
 
     return *m_testTrigger;
 }
index 00f263f..c7d933c 100644 (file)
@@ -26,7 +26,7 @@
 #include "config.h"
 #include "WheelEventDeltaFilter.h"
 
-#if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101100
+#if HAVE(NSSCROLLING_FILTERS)
 #include "WheelEventDeltaFilterMac.h"
 #endif
 
@@ -46,7 +46,7 @@ WheelEventDeltaFilter::~WheelEventDeltaFilter()
 
 std::unique_ptr<WheelEventDeltaFilter> WheelEventDeltaFilter::create()
 {
-#if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101100
+#if HAVE(NSSCROLLING_FILTERS)
     return std::make_unique<WheelEventDeltaFilterMac>();
 #else
     return std::make_unique<BasicWheelEventDeltaFilter>();
@@ -63,6 +63,11 @@ FloatSize WheelEventDeltaFilter::filteredDelta() const
     return m_currentFilteredDelta;
 }
 
+FloatPoint WheelEventDeltaFilter::filteredVelocity() const
+{
+    return m_currentFilteredVelocity;
+}
+
 BasicWheelEventDeltaFilter::BasicWheelEventDeltaFilter()
     : WheelEventDeltaFilter()
 {
index 5bd74d3..f25626f 100644 (file)
@@ -25,6 +25,7 @@
 
 #pragma once
 
+#include "FloatPoint.h"
 #include "FloatSize.h"
 #include <wtf/Deque.h>
 
@@ -39,11 +40,13 @@ public:
     WEBCORE_EXPORT virtual void updateFromDelta(const FloatSize&) = 0;
     WEBCORE_EXPORT virtual void beginFilteringDeltas() = 0;
     WEBCORE_EXPORT virtual void endFilteringDeltas() = 0;
+    WEBCORE_EXPORT FloatPoint filteredVelocity() const;
     WEBCORE_EXPORT bool isFilteringDeltas() const;
     WEBCORE_EXPORT FloatSize filteredDelta() const;
 
 protected:
     FloatSize m_currentFilteredDelta;
+    FloatPoint m_currentFilteredVelocity;
     bool m_isFilteringDeltas { false };
 };
 
index 654f21a..bfac77f 100644 (file)
@@ -25,7 +25,7 @@
 
 #include "config.h"
 
-#if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101100
+#if HAVE(NSSCROLLING_FILTERS)
 
 #include "WheelEventDeltaFilterMac.h"
 #include "FloatPoint.h"
@@ -55,6 +55,7 @@ void WheelEventDeltaFilterMac::updateFromDelta(const FloatSize& delta)
     NSPoint filteredDeltaResult;
     NSPoint filteredVelocityResult;
     [m_predominantAxisFilter filterInputDelta:NSPoint(FloatPoint(delta.width(), delta.height())) timestamp:monotonicallyIncreasingTime() - m_beginFilteringDeltasTime outputDelta:&filteredDeltaResult velocity:&filteredVelocityResult];
+    m_currentFilteredVelocity = FloatPoint(filteredVelocityResult);
     m_currentFilteredDelta = FloatSize(filteredDeltaResult.x, filteredDeltaResult.y);
 }
 
@@ -68,4 +69,4 @@ void WheelEventDeltaFilterMac::endFilteringDeltas()
 
 }
 
-#endif /* PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101100 */
+#endif /* HAVE(NSSCROLLING_FILTERS) */
diff --git a/Source/WebCore/page/scrolling/ScrollingMomentumCalculator.cpp b/Source/WebCore/page/scrolling/ScrollingMomentumCalculator.cpp
new file mode 100644 (file)
index 0000000..31d03ef
--- /dev/null
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2016 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+#include "ScrollingMomentumCalculator.h"
+
+#include "FloatPoint.h"
+#include "FloatSize.h"
+#include <wtf/CurrentTime.h>
+
+namespace WebCore {
+
+static const double scrollSnapAnimationDuration = 1;
+
+ScrollingMomentumCalculator::ScrollingMomentumCalculator(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatPoint& targetOffset, const FloatSize& initialDelta, const FloatPoint& initialVelocity)
+    : m_initialDelta(initialDelta)
+    , m_initialVelocity(initialVelocity)
+    , m_initialScrollOffset(initialOffset.x(), initialOffset.y())
+    , m_targetScrollOffset(targetOffset.x(), targetOffset.y())
+    , m_viewportSize(viewportSize)
+    , m_contentSize(contentSize)
+{
+}
+
+#if !HAVE(NSSCROLLING_FILTERS)
+
+std::unique_ptr<ScrollingMomentumCalculator> ScrollingMomentumCalculator::create(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatPoint& targetOffset, const FloatSize& initialDelta, const FloatPoint& initialVelocity)
+{
+    return std::make_unique<BasicScrollingMomentumCalculator>(viewportSize, contentSize, initialOffset, targetOffset, initialDelta, initialVelocity);
+}
+
+#endif
+
+BasicScrollingMomentumCalculator::BasicScrollingMomentumCalculator(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatPoint& targetOffset, const FloatSize& initialDelta, const FloatPoint& initialVelocity)
+    : ScrollingMomentumCalculator(viewportSize, contentSize, initialOffset, targetOffset, initialDelta, initialVelocity)
+{
+}
+
+FloatSize BasicScrollingMomentumCalculator::linearlyInterpolatedOffsetAtProgress(float progress) const
+{
+    return m_initialScrollOffset + progress * (m_targetScrollOffset - m_initialScrollOffset);
+}
+
+FloatSize BasicScrollingMomentumCalculator::cubicallyInterpolatedOffsetAtProgress(float progress) const
+{
+    ASSERT(!m_forceLinearAnimationCurve);
+    FloatSize interpolatedPoint;
+    for (int i = 0; i < 4; ++i)
+        interpolatedPoint += std::pow(progress, i) * m_snapAnimationCurveCoefficients[i];
+
+    return interpolatedPoint;
+}
+
+FloatPoint BasicScrollingMomentumCalculator::scrollOffsetAfterElapsedTime(double seconds)
+{
+    if (m_momentumCalculatorRequiresInitialization) {
+        initializeSnapProgressCurve();
+        initializeInterpolationCoefficientsIfNecessary();
+        m_momentumCalculatorRequiresInitialization = false;
+    }
+
+    float progress = animationProgressAfterElapsedTime(seconds);
+    auto offsetAsSize = m_forceLinearAnimationCurve ? linearlyInterpolatedOffsetAtProgress(progress) : cubicallyInterpolatedOffsetAtProgress(progress);
+    return FloatPoint(offsetAsSize.width(), offsetAsSize.height());
+}
+
+double BasicScrollingMomentumCalculator::animationDuration()
+{
+    return scrollSnapAnimationDuration;
+}
+
+/**
+ * Computes and sets coefficients required for interpolated snapping when scrolling in 2 dimensions, given
+ * initial conditions (the initial and target vectors, along with the initial wheel delta as a vector). The
+ * path is a cubic Bezier curve of the form p(s) = INITIAL + (C_1 * s) + (C_2 * s^2) + (C_3 * s^3) where each
+ * C_i is a 2D vector and INITIAL is the vector representing the initial scroll offset. s is a real in the
+ * interval [0, 1] indicating the "progress" of the curve (i.e. how much of the curve has been traveled).
+ *
+ * The curve has 4 control points, the first and last of which are the initial and target points, respectively.
+ * The distances between adjacent control points are constrained to be the same, making the convex hull an
+ * isosceles trapezoid with 3 sides of equal length. Additionally, the vector from the first control point to
+ * the second points in the same direction as the initial scroll delta. These constraints ensure two properties:
+ *     1. The direction of the snap animation at s=0 will be equal to the direction of the initial scroll delta.
+ *     2. Points at regular intervals of s will be evenly spread out.
+ *
+ * If the initial scroll direction is orthogonal to or points in the opposite direction as the vector from the
+ * initial point to the target point, initialization returns early and sets the curve to animate directly to the
+ * snap point without cubic interpolation.
+ *
+ * FIXME: This should be refactored to use UnitBezier.
+ */
+void BasicScrollingMomentumCalculator::initializeInterpolationCoefficientsIfNecessary()
+{
+    m_forceLinearAnimationCurve = true;
+    float initialDeltaMagnitude = m_initialDelta.diagonalLength();
+    if (initialDeltaMagnitude < 1) {
+        // The initial wheel delta is so insignificant that we're better off considering this to have the same effect as finishing a scroll gesture with no momentum.
+        // Thus, cubic interpolation isn't needed here.
+        return;
+    }
+
+    FloatSize startToEndVector = m_targetScrollOffset - m_initialScrollOffset;
+    float startToEndDistance = startToEndVector.diagonalLength();
+    if (!startToEndDistance) {
+        // The start and end positions are the same, so we shouldn't try to interpolate a path.
+        return;
+    }
+
+    float cosTheta = (m_initialDelta.width() * startToEndVector.width() + m_initialDelta.height() * startToEndVector.height()) / (initialDeltaMagnitude * startToEndDistance);
+    if (cosTheta <= 0) {
+        // It's possible that the user is not scrolling towards the target snap offset (for instance, scrolling against a corner when 2D scroll snapping).
+        // In this case, just let the scroll offset animate to the target without computing a cubic curve.
+        return;
+    }
+
+    float sideLength = startToEndDistance / (2.0f * cosTheta + 1.0f);
+    FloatSize controlVector1 = m_initialScrollOffset + sideLength * m_initialDelta / initialDeltaMagnitude;
+    FloatSize controlVector2 = controlVector1 + (sideLength * startToEndVector / startToEndDistance);
+    m_snapAnimationCurveCoefficients[0] = m_initialScrollOffset;
+    m_snapAnimationCurveCoefficients[1] = 3 * (controlVector1 - m_initialScrollOffset);
+    m_snapAnimationCurveCoefficients[2] = 3 * (m_initialScrollOffset - 2 * controlVector1 + controlVector2);
+    m_snapAnimationCurveCoefficients[3] = 3 * (controlVector1 - controlVector2) - m_initialScrollOffset + m_targetScrollOffset;
+    m_forceLinearAnimationCurve = false;
+}
+
+static const float framesPerSecond = 60.0f;
+
+/**
+ * Computes and sets parameters required for tracking the progress of a snap animation curve, interpolated
+ * or linear. The progress curve s(t) maps time t to progress s; both variables are in the interval [0, 1].
+ * The time input t is 0 when the current time is the start of the animation, t = 0, and 1 when the current
+ * time is at or after the end of the animation, t = m_scrollSnapAnimationDuration.
+ *
+ * In this exponential progress model, s(t) = A - A * b^(-kt), where k = 60T is the number of frames in the
+ * animation (assuming 60 FPS and an animation duration of T) and A, b are reals greater than or equal to 1.
+ * Also note that we are given the initial progress, a value indicating the portion of the curve which our
+ * initial scroll delta takes us. This is important when matching the initial speed of the animation to the
+ * user's initial momentum scrolling speed. Let this initial progress amount equal v_0. I clamp this initial
+ * progress amount to a minimum or maximum value.
+ *
+ * A is referred to as the curve magnitude, while b is referred to as the decay factor. We solve for A and b,
+ * keeping the following constraints in mind:
+ *     1. s(0) = 0
+ *     2. s(1) = 1
+ *     3. s(1/k) = v_0
+ *
+ * First, observe that s(0) = 0 holds for appropriate values of A, b. Solving for the remaining constraints
+ * yields a nonlinear system of two equations. In lieu of a purely analytical solution, an alternating
+ * optimization scheme is used to approximate A and b. This technique converges quickly (within 5 iterations
+ * or so) for appropriate values of v_0. The optimization terminates early when the decay factor changes by
+ * less than a threshold between one iteration and the next.
+ */
+void BasicScrollingMomentumCalculator::initializeSnapProgressCurve()
+{
+    static const int maxNumScrollSnapParameterEstimationIterations = 10;
+    static const float scrollSnapDecayFactorConvergenceThreshold = 0.001;
+    static const float initialScrollSnapCurveMagnitude = 1.1;
+    static const float minScrollSnapInitialProgress = 0.1;
+    static const float maxScrollSnapInitialProgress = 0.5;
+
+    FloatSize alignmentVector = m_initialDelta * (m_targetScrollOffset - m_initialScrollOffset);
+    float initialProgress;
+    if (alignmentVector.width() + alignmentVector.height() > 0)
+        initialProgress = clampTo(m_initialDelta.diagonalLength() / (m_targetScrollOffset - m_initialScrollOffset).diagonalLength(), minScrollSnapInitialProgress, maxScrollSnapInitialProgress);
+    else
+        initialProgress = minScrollSnapInitialProgress;
+
+    float previousDecayFactor = 1.0f;
+    m_snapAnimationCurveMagnitude = initialScrollSnapCurveMagnitude;
+    for (int i = 0; i < maxNumScrollSnapParameterEstimationIterations; ++i) {
+        m_snapAnimationDecayFactor = m_snapAnimationCurveMagnitude / (m_snapAnimationCurveMagnitude - initialProgress);
+        m_snapAnimationCurveMagnitude = 1.0f / (1.0f - std::pow(m_snapAnimationDecayFactor, -framesPerSecond * scrollSnapAnimationDuration));
+        if (std::abs(m_snapAnimationDecayFactor - previousDecayFactor) < scrollSnapDecayFactorConvergenceThreshold)
+            break;
+
+        previousDecayFactor = m_snapAnimationDecayFactor;
+    }
+}
+
+float BasicScrollingMomentumCalculator::animationProgressAfterElapsedTime(double seconds) const
+{
+    float timeProgress = clampTo<float>(seconds / scrollSnapAnimationDuration, 0, 1);
+    return std::min(1.0, m_snapAnimationCurveMagnitude * (1.0 - std::pow(m_snapAnimationDecayFactor, -framesPerSecond * scrollSnapAnimationDuration * timeProgress)));
+}
+
+}; // namespace WebCore
diff --git a/Source/WebCore/page/scrolling/ScrollingMomentumCalculator.h b/Source/WebCore/page/scrolling/ScrollingMomentumCalculator.h
new file mode 100644 (file)
index 0000000..86bd77b
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2016 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#if ENABLE(CSS_SCROLL_SNAP)
+
+#include "AxisScrollSnapOffsets.h"
+#include "PlatformWheelEvent.h"
+#include "ScrollTypes.h"
+
+namespace WebCore {
+
+class FloatPoint;
+class FloatSize;
+
+class ScrollingMomentumCalculator {
+public:
+    ScrollingMomentumCalculator(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatPoint& targetOffset, const FloatSize& initialDelta, const FloatPoint& initialVelocity);
+    static std::unique_ptr<ScrollingMomentumCalculator> create(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatPoint& targetOffset, const FloatSize& initialDelta, const FloatPoint& initialVelocity);
+    virtual ~ScrollingMomentumCalculator() { }
+
+    virtual FloatPoint scrollOffsetAfterElapsedTime(double time) = 0;
+    virtual double animationDuration() = 0;
+
+protected:
+    FloatSize m_initialDelta;
+    FloatPoint m_initialVelocity;
+    FloatSize m_initialScrollOffset;
+    FloatSize m_targetScrollOffset;
+    FloatSize m_viewportSize;
+    FloatSize m_contentSize;
+};
+
+class BasicScrollingMomentumCalculator final : public ScrollingMomentumCalculator {
+public:
+    BasicScrollingMomentumCalculator(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatPoint& targetOffset, const FloatSize& initialDelta, const FloatPoint& initialVelocity);
+
+private:
+    FloatPoint scrollOffsetAfterElapsedTime(double seconds) final;
+    double animationDuration() final;
+    void initializeInterpolationCoefficientsIfNecessary();
+    void initializeSnapProgressCurve();
+    float animationProgressAfterElapsedTime(double time) const;
+    FloatSize linearlyInterpolatedOffsetAtProgress(float progress) const;
+    FloatSize cubicallyInterpolatedOffsetAtProgress(float progress) const;
+
+    float m_snapAnimationCurveMagnitude { 0 };
+    float m_snapAnimationDecayFactor { 0 };
+    FloatSize m_snapAnimationCurveCoefficients[4] { };
+    bool m_forceLinearAnimationCurve { false };
+    bool m_momentumCalculatorRequiresInitialization { true };
+};
+
+} // namespace WebCore
+
+#endif // ENABLE(CSS_SCROLL_SNAP)
diff --git a/Source/WebCore/page/scrolling/mac/ScrollingMomentumCalculatorMac.h b/Source/WebCore/page/scrolling/mac/ScrollingMomentumCalculatorMac.h
new file mode 100644 (file)
index 0000000..af8e918
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2016 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "ScrollingMomentumCalculator.h"
+#include <wtf/RetainPtr.h>
+
+#if HAVE(NSSCROLLING_FILTERS)
+
+@class _NSScrollingMomentumCalculator;
+
+namespace WebCore {
+
+class ScrollingMomentumCalculatorMac final : public ScrollingMomentumCalculator {
+public:
+    ScrollingMomentumCalculatorMac(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatPoint& targetOffset, const FloatSize& initialDelta, const FloatPoint& initialVelocity);
+
+private:
+    FloatPoint scrollOffsetAfterElapsedTime(double time) final;
+    double animationDuration() final;
+    _NSScrollingMomentumCalculator *ensurePlatformMomentumCalculator();
+
+    RetainPtr<_NSScrollingMomentumCalculator> m_platformMomentumCalculator;
+};
+
+} // namespace WebCore
+
+#endif // HAVE(NSSCROLLING_FILTERS)
diff --git a/Source/WebCore/page/scrolling/mac/ScrollingMomentumCalculatorMac.mm b/Source/WebCore/page/scrolling/mac/ScrollingMomentumCalculatorMac.mm
new file mode 100644 (file)
index 0000000..9562d78
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2016 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+#include "ScrollingMomentumCalculatorMac.h"
+
+#if HAVE(NSSCROLLING_FILTERS)
+
+#include "NSScrollingMomentumCalculatorSPI.h"
+
+namespace WebCore {
+
+std::unique_ptr<ScrollingMomentumCalculator> ScrollingMomentumCalculator::create(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatPoint& targetOffset, const FloatSize& initialDelta, const FloatPoint& initialVelocity)
+{
+    return std::make_unique<ScrollingMomentumCalculatorMac>(viewportSize, contentSize, initialOffset, targetOffset, initialDelta, initialVelocity);
+}
+
+ScrollingMomentumCalculatorMac::ScrollingMomentumCalculatorMac(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatPoint& targetOffset, const FloatSize& initialDelta, const FloatPoint& initialVelocity)
+    : ScrollingMomentumCalculator(viewportSize, contentSize, initialOffset, targetOffset, initialDelta, initialVelocity)
+{
+}
+
+FloatPoint ScrollingMomentumCalculatorMac::scrollOffsetAfterElapsedTime(double time)
+{
+    return [ensurePlatformMomentumCalculator() positionAfterDuration:time];
+}
+
+double ScrollingMomentumCalculatorMac::animationDuration()
+{
+    return [ensurePlatformMomentumCalculator() durationUntilStop];
+}
+
+_NSScrollingMomentumCalculator *ScrollingMomentumCalculatorMac::ensurePlatformMomentumCalculator()
+{
+    if (m_platformMomentumCalculator)
+        return m_platformMomentumCalculator.get();
+
+    NSPoint origin = NSMakePoint(m_initialScrollOffset.width(), m_initialScrollOffset.height());
+    NSRect contentFrame = NSMakeRect(0, 0, m_contentSize.width(), m_contentSize.height());
+    m_platformMomentumCalculator = adoptNS([[_NSScrollingMomentumCalculator alloc] initWithInitialOrigin:origin velocity:m_initialVelocity documentFrame:contentFrame constrainedClippingOrigin:NSZeroPoint clippingSize:m_viewportSize tolerance:NSMakeSize(1, 1)]);
+    [m_platformMomentumCalculator setDestinationOrigin:NSMakePoint(m_targetScrollOffset.width(), m_targetScrollOffset.height())];
+    [m_platformMomentumCalculator calculateToReachDestination];
+    return m_platformMomentumCalculator.get();
+}
+
+} // namespace WebCore
+
+#endif // HAVE(NSSCROLLING_FILTERS)
index a7947ff..61eb99f 100644 (file)
@@ -84,12 +84,13 @@ private:
     void removeTestDeferralForReason(WheelEventTestTrigger::ScrollableAreaIdentifier, WheelEventTestTrigger::DeferTestTriggerReason) const override;
 
 #if ENABLE(CSS_SCROLL_SNAP) && PLATFORM(MAC)
-    LayoutUnit scrollOffsetOnAxis(ScrollEventAxis) const override;
+    FloatPoint scrollOffset() const override;
     void immediateScrollOnAxis(ScrollEventAxis, float delta) override;
     float pageScaleFactor() const override;
     void startScrollSnapTimer() override;
     void stopScrollSnapTimer() override;
     LayoutSize scrollExtent() const override;
+    FloatSize viewportSize() const override;
 #endif
 
     void logExposedUnfilledArea();
index d9411b4..29dbbe3 100644 (file)
@@ -581,10 +581,9 @@ static void logThreadedScrollingMode(unsigned synchronousScrollingReasons)
 }
 
 #if ENABLE(CSS_SCROLL_SNAP)
-LayoutUnit ScrollingTreeFrameScrollingNodeMac::scrollOffsetOnAxis(ScrollEventAxis axis) const
+FloatPoint ScrollingTreeFrameScrollingNodeMac::scrollOffset() const
 {
-    const FloatPoint& currentPosition = scrollPosition();
-    return axis == ScrollEventAxis::Horizontal ? currentPosition.x() : currentPosition.y();
+    return scrollPosition();
 }
 
 void ScrollingTreeFrameScrollingNodeMac::immediateScrollOnAxis(ScrollEventAxis axis, float delta)
@@ -618,6 +617,12 @@ LayoutSize ScrollingTreeFrameScrollingNodeMac::scrollExtent() const
 {
     return LayoutSize(totalContentsSize());
 }
+
+FloatSize ScrollingTreeFrameScrollingNodeMac::viewportSize() const
+{
+    return scrollableAreaSize();
+}
+
 #endif
 
 void ScrollingTreeFrameScrollingNodeMac::deferTestsForReason(WheelEventTestTrigger::ScrollableAreaIdentifier identifier, WheelEventTestTrigger::DeferTestTriggerReason reason) const
index 071355c..f8933b8 100644 (file)
@@ -26,6 +26,7 @@
 #ifndef PlatformWheelEvent_h
 #define PlatformWheelEvent_h
 
+#include "FloatPoint.h"
 #include "IntPoint.h"
 #include "PlatformEvent.h"
 #include <wtf/WindowsExtras.h>
@@ -119,11 +120,12 @@ namespace WebCore {
             return copy;
         }
 
-        PlatformWheelEvent copyWithDeltas(float deltaX, float deltaY) const
+        PlatformWheelEvent copyWithDeltasAndVelocity(float deltaX, float deltaY, FloatPoint velocity) const
         {
             PlatformWheelEvent copy = *this;
             copy.m_deltaX = deltaX;
             copy.m_deltaY = deltaY;
+            copy.m_scrollingVelocity = velocity;
             return copy;
         }
 
@@ -166,6 +168,8 @@ namespace WebCore {
         bool useLatchedEventElement() const { return false; }
 #endif
 
+        FloatPoint scrollingVelocity() const { return m_scrollingVelocity; }
+
 #if PLATFORM(WIN)
         PlatformWheelEvent(HWND, WPARAM, LPARAM, bool isMouseHWheel);
         PlatformWheelEvent(HWND, const FloatSize& delta, const FloatPoint& location);
@@ -180,6 +184,7 @@ namespace WebCore {
         float m_wheelTicksY;
         PlatformWheelEventGranularity m_granularity;
         bool m_directionInvertedFromDevice;
+        FloatPoint m_scrollingVelocity;
 #if PLATFORM(COCOA)
         bool m_hasPreciseScrollingDeltas;
         PlatformWheelEventPhase m_phase;
index b79c499..0a8bf8b 100644 (file)
@@ -201,9 +201,9 @@ void ScrollAnimator::updateScrollSnapState()
     m_scrollController.updateScrollSnapState(m_scrollableArea);
 }
 
-LayoutUnit ScrollAnimator::scrollOffsetOnAxis(ScrollEventAxis axis) const
+FloatPoint ScrollAnimator::scrollOffset() const
 {
-    return axis == ScrollEventAxis::Horizontal ? m_currentPosition.x() : m_currentPosition.y();
+    return m_currentPosition;
 }
 
 void ScrollAnimator::immediateScrollOnAxis(ScrollEventAxis axis, float delta)
@@ -221,6 +221,12 @@ LayoutSize ScrollAnimator::scrollExtent() const
 {
     return m_scrollableArea.contentsSize();
 }
+
+FloatSize ScrollAnimator::viewportSize() const
+{
+    return m_scrollableArea.visibleSize();
+}
+
 #endif
 
 #if (ENABLE(CSS_SCROLL_SNAP) || ENABLE(RUBBER_BANDING)) && PLATFORM(MAC)
index dd0f3a4..eae62fc 100644 (file)
@@ -134,11 +134,12 @@ public:
     bool processWheelEventForScrollSnap(const PlatformWheelEvent&);
 #endif
     void updateScrollSnapState();
-    LayoutUnit scrollOffsetOnAxis(ScrollEventAxis) const override;
+    FloatPoint scrollOffset() const override;
     void immediateScrollOnAxis(ScrollEventAxis, float delta) override;
     bool activeScrollSnapIndexDidChange() const;
     unsigned activeScrollSnapIndexForAxis(ScrollEventAxis) const;
     LayoutSize scrollExtent() const override;
+    FloatSize viewportSize() const override;
 #endif
 
 protected:
index 1b64812..25428bf 100644 (file)
@@ -81,7 +81,7 @@ public:
     virtual void removeTestDeferralForReason(WheelEventTestTrigger::ScrollableAreaIdentifier, WheelEventTestTrigger::DeferTestTriggerReason) const { /* Do nothing */ }
 
 #if ENABLE(CSS_SCROLL_SNAP)
-    virtual LayoutUnit scrollOffsetOnAxis(ScrollEventAxis) const = 0;
+    virtual FloatPoint scrollOffset() const = 0;
     virtual void immediateScrollOnAxis(ScrollEventAxis, float delta) = 0;
     virtual void startScrollSnapTimer()
     {
@@ -104,9 +104,21 @@ public:
     }
 
     virtual LayoutSize scrollExtent() const = 0;
+    virtual FloatSize viewportSize() const = 0;
 #endif
 };
 
+enum class WheelEventStatus {
+    UserScrollBegin,
+    UserScrolling,
+    UserScrollEnd,
+    InertialScrollBegin,
+    InertialScrolling,
+    InertialScrollEnd,
+    StatelessScrollEvent,
+    Unknown
+};
+
 class ScrollController {
     WTF_MAKE_NONCOPYABLE(ScrollController);
 
@@ -144,30 +156,25 @@ private:
 #endif
 
 #if ENABLE(CSS_SCROLL_SNAP)
-    LayoutUnit scrollOffsetOnAxis(ScrollEventAxis) const;
     void setNearestScrollSnapIndexForAxisAndOffset(ScrollEventAxis, int);
-    ScrollSnapAnimatorState& scrollSnapPointState(ScrollEventAxis);
-    const ScrollSnapAnimatorState& scrollSnapPointState(ScrollEventAxis) const;
 #if PLATFORM(MAC)
     void scrollSnapTimerFired();
     void startScrollSnapTimer();
     void stopScrollSnapTimer();
 
-    void processWheelEventForScrollSnapOnAxis(ScrollEventAxis, const PlatformWheelEvent&);
-    bool shouldOverrideWheelEvent(ScrollEventAxis, const PlatformWheelEvent&) const;
-
-    void beginScrollSnapAnimation(ScrollEventAxis, ScrollSnapState);
-    
-    void endScrollSnapAnimation(ScrollSnapState);
-    void initializeScrollSnapAnimationParameters();
-    bool isSnappingOnAxis(ScrollEventAxis) const;
-    
+    bool shouldOverrideInertialScrolling() const;
+    void statelessSnapTransitionTimerFired();
+    void startDeferringTestsDueToScrollSnapping();
+    void stopDeferringTestsDueToScrollSnapping();
+    void scheduleStatelessScrollSnap();
 #endif
 #endif
 
     ScrollControllerClient& m_client;
-    
+
+#if PLATFORM(MAC)
     CFTimeInterval m_lastMomentumScrollTimestamp { 0 };
+#endif
     FloatSize m_overflowScrollDelta;
     FloatSize m_stretchScrollForce;
     FloatSize m_momentumVelocity;
@@ -181,20 +188,21 @@ private:
 #endif
 
 #if ENABLE(CSS_SCROLL_SNAP)
-    bool m_expectingHorizontalStatelessScrollSnap { false };
-    bool m_expectingVerticalStatelessScrollSnap { false };
-    std::unique_ptr<ScrollSnapAnimatorState> m_horizontalScrollSnapState;
-    std::unique_ptr<ScrollSnapAnimatorState> m_verticalScrollSnapState;
-    std::unique_ptr<ScrollSnapAnimationCurveState> m_scrollSnapCurveState;
+    std::unique_ptr<ScrollSnapAnimatorState> m_scrollSnapState;
 #if PLATFORM(MAC)
+    FloatPoint m_dragEndedScrollingVelocity;
+    RunLoop::Timer<ScrollController> m_statelessSnapTransitionTimer;
     RunLoop::Timer<ScrollController> m_scrollSnapTimer;
 #endif
 #endif
 
+#if PLATFORM(MAC)
     bool m_inScrollGesture { false };
     bool m_momentumScrollInProgress { false };
     bool m_ignoreMomentumScrolls { false };
     bool m_snapRubberbandTimerIsActive { false };
+#endif
+
     bool m_activeScrollSnapIndexDidChange { false };
 };
     
index cd6fdbe..9b93d13 100644 (file)
@@ -74,23 +74,7 @@ static const float rubberbandDirectionLockStretchRatio = 1;
 static const float rubberbandMinimumRequiredDeltaBeforeStretch = 10;
 #endif
 
-#if ENABLE(CSS_SCROLL_SNAP) && PLATFORM(MAC)
-static const float inertialScrollPredictionFactor = 16.7;
-static const double statelessScrollSnapDelay = 0.5;
-#endif
-
 #if PLATFORM(MAC)
-enum class WheelEventStatus {
-    UserScrollBegin,
-    UserScrolling,
-    UserScrollEnd,
-    InertialScrollBegin,
-    InertialScrolling,
-    InertialScrollEnd,
-    StatelessScrollEvent,
-    Unknown
-};
-
 static float elasticDeltaForTimeDelta(float initialPosition, float initialVelocity, float elapsedTime)
 {
     return wkNSElasticDeltaForTimeDelta(initialPosition, initialVelocity, elapsedTime);
@@ -116,6 +100,11 @@ static float scrollWheelMultiplier()
     }
     return multiplier;
 }
+
+static ScrollEventAxis otherScrollEventAxis(ScrollEventAxis axis)
+{
+    return axis == ScrollEventAxis::Horizontal ? ScrollEventAxis::Vertical : ScrollEventAxis::Horizontal;
+}
 #endif
 
 ScrollController::ScrollController(ScrollControllerClient& client)
@@ -124,6 +113,7 @@ ScrollController::ScrollController(ScrollControllerClient& client)
     , m_snapRubberbandTimer(RunLoop::current(), this, &ScrollController::snapRubberBandTimerFired)
 #endif
 #if ENABLE(CSS_SCROLL_SNAP) && PLATFORM(MAC)
+    , m_statelessSnapTransitionTimer(RunLoop::current(), this, &ScrollController::statelessSnapTransitionTimerFired)
     , m_scrollSnapTimer(RunLoop::current(), this, &ScrollController::scrollSnapTimerFired)
 #endif
 {
@@ -466,21 +456,6 @@ bool ScrollController::shouldRubberBandInHorizontalDirection(const PlatformWheel
 #endif
 
 #if ENABLE(CSS_SCROLL_SNAP)
-ScrollSnapAnimatorState& ScrollController::scrollSnapPointState(ScrollEventAxis axis)
-{
-    ASSERT(axis != ScrollEventAxis::Horizontal || m_horizontalScrollSnapState);
-    ASSERT(axis != ScrollEventAxis::Vertical || m_verticalScrollSnapState);
-
-    return (axis == ScrollEventAxis::Horizontal) ? *m_horizontalScrollSnapState : *m_verticalScrollSnapState;
-}
-
-const ScrollSnapAnimatorState& ScrollController::scrollSnapPointState(ScrollEventAxis axis) const
-{
-    ASSERT(axis != ScrollEventAxis::Horizontal || m_horizontalScrollSnapState);
-    ASSERT(axis != ScrollEventAxis::Vertical || m_verticalScrollSnapState);
-    
-    return (axis == ScrollEventAxis::Horizontal) ? *m_horizontalScrollSnapState : *m_verticalScrollSnapState;
-}
 
 #if PLATFORM(MAC)
 static inline WheelEventStatus toWheelEventStatus(PlatformWheelEventPhase phase, PlatformWheelEventPhase momentumPhase)
@@ -523,355 +498,201 @@ static inline WheelEventStatus toWheelEventStatus(PlatformWheelEventPhase phase,
     return WheelEventStatus::Unknown;
 }
 
-void ScrollController::processWheelEventForScrollSnapOnAxis(ScrollEventAxis axis, const PlatformWheelEvent& event)
+bool ScrollController::shouldOverrideInertialScrolling() const
 {
-    ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);
+    if (!m_scrollSnapState)
+        return false;
 
-    float wheelDelta = axis == ScrollEventAxis::Horizontal ? -event.deltaX() : -event.deltaY();
-    WheelEventStatus wheelStatus = toWheelEventStatus(event.phase(), event.momentumPhase());
-    
-    switch (wheelStatus) {
+    ScrollSnapState scrollSnapState = m_scrollSnapState->currentState();
+    return scrollSnapState == ScrollSnapState::Gliding || scrollSnapState == ScrollSnapState::DestinationReached;
+}
+
+void ScrollController::scheduleStatelessScrollSnap()
+{
+    stopScrollSnapTimer();
+    m_statelessSnapTransitionTimer.stop();
+    if (!m_scrollSnapState)
+        return;
+
+    static const double statelessScrollSnapDelay = 0.75;
+    m_statelessSnapTransitionTimer.startOneShot(statelessScrollSnapDelay);
+    startDeferringTestsDueToScrollSnapping();
+}
+
+void ScrollController::statelessSnapTransitionTimerFired()
+{
+    if (!m_scrollSnapState)
+        return;
+
+    m_scrollSnapState->transitionToSnapAnimationState(m_client.scrollExtent(), m_client.viewportSize(), m_client.pageScaleFactor(), m_client.scrollOffset());
+    startScrollSnapTimer();
+}
+
+void ScrollController::startDeferringTestsDueToScrollSnapping()
+{
+    m_client.deferTestsForReason(reinterpret_cast<WheelEventTestTrigger::ScrollableAreaIdentifier>(this), WheelEventTestTrigger::ScrollSnapInProgress);
+}
+
+void ScrollController::stopDeferringTestsDueToScrollSnapping()
+{
+    m_client.removeTestDeferralForReason(reinterpret_cast<WheelEventTestTrigger::ScrollableAreaIdentifier>(this), WheelEventTestTrigger::ScrollSnapInProgress);
+}
+
+bool ScrollController::processWheelEventForScrollSnap(const PlatformWheelEvent& wheelEvent)
+{
+    if (!m_scrollSnapState)
+        return true;
+
+    if (m_scrollSnapState->snapOffsetsForAxis(ScrollEventAxis::Horizontal).isEmpty() && m_scrollSnapState->snapOffsetsForAxis(ScrollEventAxis::Vertical).isEmpty())
+        return true;
+
+    WheelEventStatus status = toWheelEventStatus(wheelEvent.phase(), wheelEvent.momentumPhase());
+    bool isInertialScrolling = false;
+    switch (status) {
     case WheelEventStatus::UserScrollBegin:
     case WheelEventStatus::UserScrolling:
-        endScrollSnapAnimation(ScrollSnapState::UserInteraction);
+        stopScrollSnapTimer();
+        m_scrollSnapState->transitionToUserInteractionState();
         break;
-            
     case WheelEventStatus::UserScrollEnd:
-        beginScrollSnapAnimation(axis, ScrollSnapState::Snapping);
+        m_dragEndedScrollingVelocity = -wheelEvent.scrollingVelocity();
+        m_scrollSnapState->transitionToSnapAnimationState(m_client.scrollExtent(), m_client.viewportSize(), m_client.pageScaleFactor(), m_client.scrollOffset());
+        startScrollSnapTimer();
         break;
-        
     case WheelEventStatus::InertialScrollBegin:
-        // Begin tracking wheel deltas for glide prediction.
-        endScrollSnapAnimation(ScrollSnapState::UserInteraction);
-        snapState.pushInitialWheelDelta(wheelDelta);
-        snapState.m_beginTrackingWheelDeltaOffset = m_client.scrollOffsetOnAxis(axis);
+        m_scrollSnapState->transitionToGlideAnimationState(m_client.scrollExtent(), m_client.viewportSize(), m_client.pageScaleFactor(), m_client.scrollOffset(), m_dragEndedScrollingVelocity, FloatSize(-wheelEvent.deltaX(), -wheelEvent.deltaY()));
+        isInertialScrolling = true;
         break;
-            
     case WheelEventStatus::InertialScrolling:
-        // This check for DestinationReached ensures that we don't receive another set of momentum events after ending the last glide.
-        if (snapState.m_currentState != ScrollSnapState::Gliding && snapState.m_currentState != ScrollSnapState::DestinationReached) {
-            if (snapState.wheelDeltaTrackingIsInProgress() && wheelDelta)
-                snapState.pushInitialWheelDelta(wheelDelta);
-            
-            if (snapState.hasFinishedTrackingWheelDeltas() && snapState.averageInitialWheelDelta())
-                beginScrollSnapAnimation(axis, ScrollSnapState::Gliding);
-        }
-        break;
-        
     case WheelEventStatus::InertialScrollEnd:
-        if (snapState.wheelDeltaTrackingIsInProgress() && snapState.averageInitialWheelDelta())
-            beginScrollSnapAnimation(axis, ScrollSnapState::Gliding);
-
-        snapState.clearInitialWheelDeltaWindow();
-        snapState.m_shouldOverrideWheelEvent = false;
+        isInertialScrolling = true;
         break;
-
     case WheelEventStatus::StatelessScrollEvent:
-        endScrollSnapAnimation(ScrollSnapState::UserInteraction);
-        snapState.clearInitialWheelDeltaWindow();
-        snapState.m_shouldOverrideWheelEvent = false;
-        m_scrollSnapTimer.startOneShot(statelessScrollSnapDelay);
-        if (axis == ScrollEventAxis::Horizontal)
-            m_expectingHorizontalStatelessScrollSnap = true;
-        else
-            m_expectingVerticalStatelessScrollSnap = true;
+        m_scrollSnapState->transitionToUserInteractionState();
+        scheduleStatelessScrollSnap();
         break;
-
     case WheelEventStatus::Unknown:
         ASSERT_NOT_REACHED();
         break;
     }
-}
 
-bool ScrollController::shouldOverrideWheelEvent(ScrollEventAxis axis, const PlatformWheelEvent& event) const
-{
-    const ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);
-
-    return snapState.m_shouldOverrideWheelEvent && toWheelEventStatus(event.phase(), event.momentumPhase()) == WheelEventStatus::InertialScrolling;
+    return !(isInertialScrolling && shouldOverrideInertialScrolling());
 }
 
-bool ScrollController::processWheelEventForScrollSnap(const PlatformWheelEvent& wheelEvent)
-{
-    bool shouldAllowWheelEventToPropagate = true;
-    if (m_verticalScrollSnapState) {
-        processWheelEventForScrollSnapOnAxis(ScrollEventAxis::Vertical, wheelEvent);
-        shouldAllowWheelEventToPropagate &= !shouldOverrideWheelEvent(ScrollEventAxis::Vertical, wheelEvent);
-    }
-    if (m_horizontalScrollSnapState) {
-        processWheelEventForScrollSnapOnAxis(ScrollEventAxis::Horizontal, wheelEvent);
-        shouldAllowWheelEventToPropagate &= !shouldOverrideWheelEvent(ScrollEventAxis::Horizontal, wheelEvent);
-    }
-    return shouldAllowWheelEventToPropagate;
-}
-#endif
-
 void ScrollController::updateScrollSnapState(const ScrollableArea& scrollableArea)
 {
-    // FIXME: Currently, scroll snap animators are recreated even though the snap offsets alone can be updated.
-    if (scrollableArea.horizontalSnapOffsets())
-        m_horizontalScrollSnapState = std::make_unique<ScrollSnapAnimatorState>(ScrollEventAxis::Horizontal, *scrollableArea.horizontalSnapOffsets());
-    else if (m_horizontalScrollSnapState)
-        m_horizontalScrollSnapState = nullptr;
+    if (auto* snapOffsets = scrollableArea.horizontalSnapOffsets())
+        updateScrollSnapPoints(ScrollEventAxis::Horizontal, *snapOffsets);
 
-    if (scrollableArea.verticalSnapOffsets())
-        m_verticalScrollSnapState = std::make_unique<ScrollSnapAnimatorState>(ScrollEventAxis::Vertical, *scrollableArea.verticalSnapOffsets());
-    else if (m_verticalScrollSnapState)
-        m_verticalScrollSnapState = nullptr;
+    if (auto* snapOffsets = scrollableArea.verticalSnapOffsets())
+        updateScrollSnapPoints(ScrollEventAxis::Vertical, *snapOffsets);
 }
 
 void ScrollController::updateScrollSnapPoints(ScrollEventAxis axis, const Vector<LayoutUnit>& snapPoints)
 {
-    // FIXME: Currently, scroll snap animators are recreated even though the snap offsets alone can be updated.
-    if (axis == ScrollEventAxis::Horizontal)
-        m_horizontalScrollSnapState = !snapPoints.isEmpty() ? std::make_unique<ScrollSnapAnimatorState>(ScrollEventAxis::Horizontal, snapPoints) : nullptr;
+    if (!m_scrollSnapState) {
+        if (snapPoints.isEmpty())
+            return;
 
-    if (axis == ScrollEventAxis::Vertical)
-        m_verticalScrollSnapState = !snapPoints.isEmpty() ? std::make_unique<ScrollSnapAnimatorState>(ScrollEventAxis::Vertical, snapPoints) : nullptr;
+        m_scrollSnapState = std::make_unique<ScrollSnapAnimatorState>();
+    }
+
+    if (snapPoints.isEmpty() && m_scrollSnapState->snapOffsetsForAxis(otherScrollEventAxis(axis)).isEmpty())
+        m_scrollSnapState = nullptr;
+    else
+        m_scrollSnapState->setSnapOffsetsForAxis(axis, snapPoints);
 }
 
-#if PLATFORM(MAC)
 void ScrollController::startScrollSnapTimer()
 {
-    if (!m_scrollSnapTimer.isActive()) {
-        m_client.startScrollSnapTimer();
-        m_scrollSnapTimer.startRepeating(1.0 / 60.0);
-    }
-
-    if (!m_scrollSnapTimer.isActive())
+    if (m_scrollSnapTimer.isActive())
         return;
 
-    m_client.deferTestsForReason(reinterpret_cast<WheelEventTestTrigger::ScrollableAreaIdentifier>(this), WheelEventTestTrigger::ScrollSnapInProgress);
+    startDeferringTestsDueToScrollSnapping();
+    m_client.startScrollSnapTimer();
+    m_scrollSnapTimer.startRepeating(1.0 / 60.0);
 }
 
 void ScrollController::stopScrollSnapTimer()
 {
-    m_client.stopScrollSnapTimer();
-    m_scrollSnapTimer.stop();
-    
-    if (m_scrollSnapTimer.isActive())
+    if (!m_scrollSnapTimer.isActive())
         return;
 
-    m_client.removeTestDeferralForReason(reinterpret_cast<WheelEventTestTrigger::ScrollableAreaIdentifier>(this), WheelEventTestTrigger::ScrollSnapInProgress);
+    stopDeferringTestsDueToScrollSnapping();
+    m_client.stopScrollSnapTimer();
+    m_scrollSnapTimer.stop();
 }
 
 void ScrollController::scrollSnapTimerFired()
 {
-    if (m_expectingHorizontalStatelessScrollSnap || m_expectingVerticalStatelessScrollSnap) {
-        if (m_expectingHorizontalStatelessScrollSnap)
-            beginScrollSnapAnimation(ScrollEventAxis::Horizontal, ScrollSnapState::Snapping);
-        if (m_expectingVerticalStatelessScrollSnap)
-            beginScrollSnapAnimation(ScrollEventAxis::Vertical, ScrollSnapState::Snapping);
-        return;
-    }
-        
-    bool snapOnHorizontalAxis = isSnappingOnAxis(ScrollEventAxis::Horizontal);
-    bool snapOnVerticalAxis = isSnappingOnAxis(ScrollEventAxis::Vertical);
-    if (snapOnHorizontalAxis && !m_horizontalScrollSnapState->canReachTargetWithCurrentInitialScrollDelta()) {
-        m_horizontalScrollSnapState->m_currentState = ScrollSnapState::DestinationReached;
-        snapOnHorizontalAxis = false;
-    }
-    if (snapOnVerticalAxis && !m_verticalScrollSnapState->canReachTargetWithCurrentInitialScrollDelta()) {
-        m_verticalScrollSnapState->m_currentState = ScrollSnapState::DestinationReached;
-        snapOnVerticalAxis = false;
-    }
-    if (!snapOnHorizontalAxis && !snapOnVerticalAxis) {
-        endScrollSnapAnimation(ScrollSnapState::DestinationReached);
-        return;
-    }
-    
-    double currentTime = monotonicallyIncreasingTime();
-    if (m_scrollSnapCurveState->shouldCompleteSnapAnimationImmediatelyAtTime(currentTime)) {
-        float finalHorizontalDelta = 0;
-        float finalVerticalDelta = 0;
-        if (snapOnHorizontalAxis)
-            finalHorizontalDelta = m_horizontalScrollSnapState->m_targetOffset - m_client.scrollOffsetOnAxis(ScrollEventAxis::Horizontal);
-        if (snapOnVerticalAxis)
-            finalVerticalDelta = m_verticalScrollSnapState->m_targetOffset - m_client.scrollOffsetOnAxis(ScrollEventAxis::Vertical);
-
-        if (finalHorizontalDelta || finalVerticalDelta)
-            m_client.immediateScrollBy(FloatSize(finalHorizontalDelta, finalVerticalDelta));
-
-        endScrollSnapAnimation(ScrollSnapState::DestinationReached);
+    if (!m_scrollSnapState) {
+        ASSERT_NOT_REACHED();
         return;
     }
-    
-    float animationProgress = m_scrollSnapCurveState->animationProgressAtTime(currentTime);
-    float horizontalDelta = 0;
-    float verticalDelta = 0;
-    if (m_scrollSnapCurveState->shouldAnimateDirectlyToSnapPoint) {
-        if (snapOnHorizontalAxis)
-            horizontalDelta = m_horizontalScrollSnapState->interpolatedOffsetAtProgress(animationProgress) - m_client.scrollOffsetOnAxis(ScrollEventAxis::Horizontal);
-        if (snapOnVerticalAxis)
-            verticalDelta = m_verticalScrollSnapState->interpolatedOffsetAtProgress(animationProgress) - m_client.scrollOffsetOnAxis(ScrollEventAxis::Vertical);
 
-    } else {
-        FloatPoint interpolatedPoint = m_scrollSnapCurveState->interpolatedPositionAtProgress(animationProgress);
-        horizontalDelta = interpolatedPoint.x() - m_client.scrollOffsetOnAxis(ScrollEventAxis::Horizontal);
-        verticalDelta = interpolatedPoint.y() - m_client.scrollOffsetOnAxis(ScrollEventAxis::Vertical);
+    bool isAnimationComplete;
+    auto animationOffset = m_scrollSnapState->currentAnimatedScrollOffset(isAnimationComplete);
+    auto currentOffset = m_client.scrollOffset();
+    m_client.immediateScrollByWithoutContentEdgeConstraints(FloatSize(animationOffset.x() - currentOffset.x(), animationOffset.y() - currentOffset.y()));
+    if (isAnimationComplete) {
+        m_scrollSnapState->transitionToDestinationReachedState();
+        stopScrollSnapTimer();
     }
-    
-    if (horizontalDelta || verticalDelta)
-        m_client.immediateScrollBy(FloatSize(horizontalDelta, verticalDelta));
 }
-
-static inline float projectedInertialScrollDistance(float initialWheelDelta)
+#else
+void ScrollController::updateScrollSnapState(const ScrollableArea&)
 {
-    // FIXME: Experiments with inertial scrolling show a fairly consistent linear relationship between initial wheel delta and total distance scrolled.
-    // In the future, we'll want to find a more accurate way of inertial scroll prediction.
-    return inertialScrollPredictionFactor * initialWheelDelta;
 }
-#endif
+#endif // PLATFORM(MAC)
 
 unsigned ScrollController::activeScrollSnapIndexForAxis(ScrollEventAxis axis) const
 {
-    if ((axis == ScrollEventAxis::Horizontal) && !m_horizontalScrollSnapState)
+    if (!m_scrollSnapState)
         return 0;
-    if ((axis == ScrollEventAxis::Vertical) && !m_verticalScrollSnapState)
-        return 0;
-    
-    const ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);
-    return snapState.m_activeSnapIndex;
+
+    return m_scrollSnapState->activeSnapIndexForAxis(axis);
 }
 
 void ScrollController::setActiveScrollSnapIndexForAxis(ScrollEventAxis axis, unsigned index)
 {
-    auto* snapState = (axis == ScrollEventAxis::Horizontal) ? m_horizontalScrollSnapState.get() : m_verticalScrollSnapState.get();
-    if (!snapState)
+    if (!m_scrollSnapState)
         return;
 
-    snapState->m_activeSnapIndex = index;
+    m_scrollSnapState->setActiveSnapIndexForAxis(axis, index);
 }
 
 void ScrollController::setNearestScrollSnapIndexForAxisAndOffset(ScrollEventAxis axis, int offset)
 {
-    float scaleFactor = m_client.pageScaleFactor();
-    ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);
-    
-    LayoutUnit clampedOffset = std::min(std::max(LayoutUnit(offset / scaleFactor), snapState.m_snapOffsets.first()), snapState.m_snapOffsets.last());
-
-    unsigned activeIndex = 0;
-    (void)closestSnapOffset<LayoutUnit, float>(snapState.m_snapOffsets, clampedOffset, 0, activeIndex);
-
-    if (activeIndex == snapState.m_activeSnapIndex)
+    if (!m_scrollSnapState)
         return;
 
-    m_activeScrollSnapIndexDidChange = true;
-    snapState.m_activeSnapIndex = activeIndex;
-}
+    float scaleFactor = m_client.pageScaleFactor();
+    ScrollSnapAnimatorState& snapState = *m_scrollSnapState;
 
-void ScrollController::setActiveScrollSnapIndicesForOffset(int x, int y)
-{
-    if (m_horizontalScrollSnapState)
-        setNearestScrollSnapIndexForAxisAndOffset(ScrollEventAxis::Horizontal, x);
-    if (m_verticalScrollSnapState)
-        setNearestScrollSnapIndexForAxisAndOffset(ScrollEventAxis::Vertical, y);
-}
+    auto snapOffsets = snapState.snapOffsetsForAxis(axis);
+    if (!snapOffsets.size())
+        return;
 
-#if PLATFORM(MAC)
-void ScrollController::beginScrollSnapAnimation(ScrollEventAxis axis, ScrollSnapState newState)
-{
-    ASSERT(newState == ScrollSnapState::Gliding || newState == ScrollSnapState::Snapping);
-    if (m_expectingHorizontalStatelessScrollSnap || m_expectingVerticalStatelessScrollSnap) {
-        m_expectingHorizontalStatelessScrollSnap = false;
-        m_expectingVerticalStatelessScrollSnap = false;
-        stopScrollSnapTimer();
-    }
-    ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);
+    LayoutUnit clampedOffset = std::min(std::max(LayoutUnit(offset / scaleFactor), snapOffsets.first()), snapOffsets.last());
 
-    LayoutUnit offset = m_client.scrollOffsetOnAxis(axis);
-    float initialWheelDelta = newState == ScrollSnapState::Gliding ? snapState.averageInitialWheelDelta() : 0;
-    LayoutUnit scaledProjectedScrollDestination = newState == ScrollSnapState::Gliding ? snapState.m_beginTrackingWheelDeltaOffset + LayoutUnit(projectedInertialScrollDistance(initialWheelDelta)) : offset;
-    if (snapState.m_snapOffsets.isEmpty())
-        return;
+    unsigned activeIndex = 0;
+    closestSnapOffset<LayoutUnit, float>(snapState.snapOffsetsForAxis(axis), clampedOffset, 0, activeIndex);
 
-    float scaleFactor = m_client.pageScaleFactor();
-    LayoutUnit originalProjectedScrollDestination = scaledProjectedScrollDestination / scaleFactor;
-    
-    LayoutUnit clampedScrollDestination = std::min(std::max(originalProjectedScrollDestination, snapState.m_snapOffsets.first()), snapState.m_snapOffsets.last());
-    snapState.m_initialOffset = offset;
-    m_activeScrollSnapIndexDidChange = false;
-    snapState.m_targetOffset = scaleFactor * closestSnapOffset<LayoutUnit, float>(snapState.m_snapOffsets, clampedScrollDestination, initialWheelDelta, snapState.m_activeSnapIndex);
-    if (snapState.m_initialOffset == snapState.m_targetOffset)
+    if (activeIndex == activeScrollSnapIndexForAxis(axis))
         return;
 
-    LayoutUnit scrollExtent = (axis == ScrollEventAxis::Horizontal) ? m_client.scrollExtent().width() : m_client.scrollExtent().height();
-    LayoutUnit projectedScrollDestination = clampedScrollDestination;
-    if (originalProjectedScrollDestination < 0 || originalProjectedScrollDestination > scrollExtent)
-        projectedScrollDestination = originalProjectedScrollDestination;
-    
     m_activeScrollSnapIndexDidChange = true;
-    snapState.m_currentState = newState;
-    if (newState == ScrollSnapState::Gliding) {
-        // Check if the other scroll axis needs to animate to the nearest snap point.
-        snapState.m_initialScrollDelta = initialWheelDelta;
-        snapState.m_shouldOverrideWheelEvent = true;
-        snapState.clearInitialWheelDeltaWindow();
-        ScrollEventAxis otherAxis = axis == ScrollEventAxis::Horizontal ? ScrollEventAxis::Vertical : ScrollEventAxis::Horizontal;
-        if ((otherAxis == ScrollEventAxis::Horizontal && m_horizontalScrollSnapState && m_horizontalScrollSnapState->m_currentState == ScrollSnapState::UserInteraction)
-            || (otherAxis == ScrollEventAxis::Vertical && m_verticalScrollSnapState && m_verticalScrollSnapState->m_currentState == ScrollSnapState::UserInteraction)) {
-            
-            ScrollSnapAnimatorState& otherState = scrollSnapPointState(otherAxis);
-            if (!otherState.averageInitialWheelDelta()) {
-                float offsetOnOtherAxis = m_client.scrollOffsetOnAxis(otherAxis);
-                float snapOffsetForOtherAxis = scaleFactor * closestSnapOffset<LayoutUnit, float>(otherState.m_snapOffsets, offsetOnOtherAxis, 0, otherState.m_activeSnapIndex);
-                if (offsetOnOtherAxis != snapOffsetForOtherAxis) {
-                    otherState.m_initialOffset = offsetOnOtherAxis;
-                    otherState.m_targetOffset = snapOffsetForOtherAxis;
-                    otherState.m_initialScrollDelta = 0;
-                    otherState.m_currentState = ScrollSnapState::Gliding;
-                }
-            }
-        }
-        
-    } else {
-        snapState.m_initialScrollDelta = initialWheelDelta;
-    }
-    initializeScrollSnapAnimationParameters();
-    startScrollSnapTimer();
-}
-
-void ScrollController::endScrollSnapAnimation(ScrollSnapState newState)
-{
-    ASSERT(newState == ScrollSnapState::DestinationReached || newState == ScrollSnapState::UserInteraction);
-    if (m_horizontalScrollSnapState)
-        m_horizontalScrollSnapState->m_currentState = newState;
-
-    if (m_verticalScrollSnapState)
-        m_verticalScrollSnapState->m_currentState = newState;
-
-    stopScrollSnapTimer();
+    setActiveScrollSnapIndexForAxis(axis, activeIndex);
 }
 
-void ScrollController::initializeScrollSnapAnimationParameters()
-{
-    if (!m_scrollSnapCurveState)
-        m_scrollSnapCurveState = std::make_unique<ScrollSnapAnimationCurveState>();
-    
-    bool isSnappingOnHorizontalAxis = isSnappingOnAxis(ScrollEventAxis::Horizontal);
-    bool isSnappingOnVerticalAxis = isSnappingOnAxis(ScrollEventAxis::Vertical);
-    FloatSize initialVector(isSnappingOnHorizontalAxis ? m_horizontalScrollSnapState->m_initialOffset : m_client.scrollOffsetOnAxis(ScrollEventAxis::Horizontal),
-        isSnappingOnVerticalAxis ? m_verticalScrollSnapState->m_initialOffset : m_client.scrollOffsetOnAxis(ScrollEventAxis::Vertical));
-    FloatSize targetVector(isSnappingOnHorizontalAxis ? m_horizontalScrollSnapState->m_targetOffset : m_client.scrollOffsetOnAxis(ScrollEventAxis::Horizontal),
-        isSnappingOnVerticalAxis ? m_verticalScrollSnapState->m_targetOffset : m_client.scrollOffsetOnAxis(ScrollEventAxis::Vertical));
-    FloatSize initialDelta(isSnappingOnHorizontalAxis ? m_horizontalScrollSnapState->m_initialScrollDelta : 0,
-        isSnappingOnVerticalAxis ? m_verticalScrollSnapState->m_initialScrollDelta : 0);
-
-    // Animate directly by default. This flag will be changed as necessary if interpolation is possible.
-    m_scrollSnapCurveState->shouldAnimateDirectlyToSnapPoint = true;
-    m_scrollSnapCurveState->initializeSnapProgressCurve(initialVector, targetVector, initialDelta);
-    if (isSnappingOnHorizontalAxis && isSnappingOnVerticalAxis)
-        m_scrollSnapCurveState->initializeInterpolationCoefficientsIfNecessary(initialVector, targetVector, initialDelta);
-}
-    
-bool ScrollController::isSnappingOnAxis(ScrollEventAxis axis) const
+void ScrollController::setActiveScrollSnapIndicesForOffset(int x, int y)
 {
-    if (axis == ScrollEventAxis::Horizontal)
-        return m_horizontalScrollSnapState && m_horizontalScrollSnapState->isSnapping();
+    if (!m_scrollSnapState)
+        return;
 
-    return m_verticalScrollSnapState && m_verticalScrollSnapState->isSnapping();
+    setNearestScrollSnapIndexForAxisAndOffset(ScrollEventAxis::Horizontal, x);
+    setNearestScrollSnapIndexForAxisAndOffset(ScrollEventAxis::Vertical, y);
 }
-    
-#endif
 #endif
 
 } // namespace WebCore
index 1330105..005be39 100644 (file)
 #include "AxisScrollSnapOffsets.h"
 #include "FloatPoint.h"
 #include "FloatSize.h"
-#include "LayoutUnit.h"
+#include "LayoutPoint.h"
 #include "PlatformWheelEvent.h"
 #include "ScrollTypes.h"
+#include "ScrollingMomentumCalculator.h"
 
 namespace WebCore {
 
@@ -44,57 +45,60 @@ enum class ScrollSnapState {
     UserInteraction
 };
 
-struct ScrollSnapAnimatorState {
-    ScrollSnapAnimatorState(ScrollEventAxis, const Vector<LayoutUnit>&);
-
-    void pushInitialWheelDelta(float);
-    float averageInitialWheelDelta() const;
-    void clearInitialWheelDeltaWindow();
-    bool isSnapping() const;
-    bool canReachTargetWithCurrentInitialScrollDelta() const;
-    bool wheelDeltaTrackingIsInProgress() const;
-    bool hasFinishedTrackingWheelDeltas() const;
-    float interpolatedOffsetAtProgress(float) const;
-    
-    static const int wheelDeltaWindowSize = 3;
-
-    Vector<LayoutUnit> m_snapOffsets;
-    ScrollEventAxis m_axis;
-    // Used to track both snapping and gliding behaviors.
-    ScrollSnapState m_currentState;
-    LayoutUnit m_initialOffset;
-    LayoutUnit m_targetOffset;
-    // Used to track gliding behavior.
-    LayoutUnit m_beginTrackingWheelDeltaOffset;
-    int m_numWheelDeltasTracked { 0 };
-    unsigned m_activeSnapIndex { 0 };
-    float m_wheelDeltaWindow[wheelDeltaWindowSize];
-    float m_initialScrollDelta { 0 };
-    bool m_shouldOverrideWheelEvent { false };
-};
-    
-/**
- * Stores state variables necessary to coordinate snapping animations between
- * horizontal and vertical axes.
- */
-struct ScrollSnapAnimationCurveState {
-    
-    void initializeSnapProgressCurve(const FloatSize&, const FloatSize&, const FloatSize&);
-    void initializeInterpolationCoefficientsIfNecessary(const FloatSize&, const FloatSize&, const FloatSize&);
-    FloatPoint interpolatedPositionAtProgress(float) const;
-    bool shouldCompleteSnapAnimationImmediatelyAtTime(double) const;
-    float animationProgressAtTime(double) const;
-
-    bool shouldAnimateDirectlyToSnapPoint { false };
-    
+class ScrollSnapAnimatorState {
+public:
+    Vector<LayoutUnit> snapOffsetsForAxis(ScrollEventAxis axis) const
+    {
+        return axis == ScrollEventAxis::Horizontal ? m_snapOffsetsX : m_snapOffsetsY;
+    }
+
+    void setSnapOffsetsForAxis(ScrollEventAxis axis, const Vector<LayoutUnit>& snapOffsets)
+    {
+        if (axis == ScrollEventAxis::Horizontal)
+            m_snapOffsetsX = snapOffsets;
+        else
+            m_snapOffsetsY = snapOffsets;
+    }
+
+    ScrollSnapState currentState() const { return m_currentState; }
+
+    unsigned activeSnapIndexForAxis(ScrollEventAxis axis) const
+    {
+        return axis == ScrollEventAxis::Horizontal ? m_activeSnapIndexX : m_activeSnapIndexY;
+    }
+
+    void setActiveSnapIndexForAxis(ScrollEventAxis axis, unsigned index)
+    {
+        if (axis == ScrollEventAxis::Horizontal)
+            m_activeSnapIndexX = index;
+        else
+            m_activeSnapIndexY = index;
+    }
+
+    FloatPoint currentAnimatedScrollOffset(bool& isAnimationComplete) const;
+
+    // State transition helpers.
+    void transitionToSnapAnimationState(const FloatSize& contentSize, const FloatSize& viewportSize, float pageScale, const FloatPoint& initialOffset);
+    void transitionToGlideAnimationState(const FloatSize& contentSize, const FloatSize& viewportSize, float pageScale, const FloatPoint& initialOffset, const FloatPoint& initialVelocity, const FloatSize& initialDelta);
+    void transitionToUserInteractionState();
+    void transitionToDestinationReachedState();
+
 private:
+    float targetOffsetForStartOffset(ScrollEventAxis, float maxScrollOffset, float startOffset, float pageScale, float delta, unsigned& outActiveSnapIndex) const;
+    void teardownAnimationForState(ScrollSnapState);
+    void setupAnimationForState(ScrollSnapState, const FloatSize& contentSize, const FloatSize& viewportSize, float pageScale, const FloatPoint& initialOffset, const FloatPoint& initialVelocity, const FloatSize& initialDelta);
+
+    ScrollSnapState m_currentState { ScrollSnapState::UserInteraction };
+
+    Vector<LayoutUnit> m_snapOffsetsX;
+    unsigned m_activeSnapIndexX { 0 };
+    Vector<LayoutUnit> m_snapOffsetsY;
+    unsigned m_activeSnapIndexY { 0 };
+
     double m_startTime { 0 };
-    float m_snapAnimationCurveMagnitude { 0 };
-    float m_snapAnimationDecayFactor { 0 };
-    FloatSize m_snapAnimationCurveCoefficients[4] { };
+    std::unique_ptr<ScrollingMomentumCalculator> m_momentumCalculator;
 };
 
-
 } // namespace WebCore
 
 #endif // ENABLE(CSS_SCROLL_SNAP)
index 7fa6432..d661de6 100644 (file)
 #include "config.h"
 #include "ScrollSnapAnimatorState.h"
 #include <wtf/CurrentTime.h>
+#include <wtf/MathExtras.h>
 
 #if ENABLE(CSS_SCROLL_SNAP)
 
 namespace WebCore {
 
-ScrollSnapAnimatorState::ScrollSnapAnimatorState(ScrollEventAxis axis, const Vector<LayoutUnit>& snapOffsets)
-    : m_snapOffsets(snapOffsets)
-    , m_axis(axis)
-    , m_currentState(ScrollSnapState::DestinationReached)
-    , m_initialOffset(0)
-    , m_targetOffset(0)
-    , m_beginTrackingWheelDeltaOffset(0)
+static const float inertialScrollPredictionFactor = 10;
+static inline float projectedInertialScrollDistance(float initialWheelDelta)
 {
+    return inertialScrollPredictionFactor * initialWheelDelta;
 }
 
-void ScrollSnapAnimatorState::pushInitialWheelDelta(float wheelDelta)
+void ScrollSnapAnimatorState::transitionToSnapAnimationState(const FloatSize& contentSize, const FloatSize& viewportSize, float pageScale, const FloatPoint& initialOffset)
 {
-    if (m_numWheelDeltasTracked < wheelDeltaWindowSize)
-        m_wheelDeltaWindow[m_numWheelDeltasTracked++] = wheelDelta;
+    setupAnimationForState(ScrollSnapState::Snapping, contentSize, viewportSize, pageScale, initialOffset, { }, { });
 }
 
-float ScrollSnapAnimatorState::averageInitialWheelDelta() const
+void ScrollSnapAnimatorState::transitionToGlideAnimationState(const FloatSize& contentSize, const FloatSize& viewportSize, float pageScale, const FloatPoint& initialOffset, const FloatPoint& initialVelocity, const FloatSize& initialDelta)
 {
-    if (!m_numWheelDeltasTracked)
-        return 0;
-
-    float sum = 0;
-    int numZeroDeltas = 0;
-    for (int i = 0; i < m_numWheelDeltasTracked; ++i) {
-        sum += m_wheelDeltaWindow[i];
-        if (!m_wheelDeltaWindow[i])
-            numZeroDeltas++;
-    }
-
-    return m_numWheelDeltasTracked == numZeroDeltas ? 0 : sum / (m_numWheelDeltasTracked - numZeroDeltas);
+    setupAnimationForState(ScrollSnapState::Gliding, contentSize, viewportSize, pageScale, initialOffset, initialVelocity, initialDelta);
 }
 
-void ScrollSnapAnimatorState::clearInitialWheelDeltaWindow()
+void ScrollSnapAnimatorState::setupAnimationForState(ScrollSnapState state, const FloatSize& contentSize, const FloatSize& viewportSize, float pageScale, const FloatPoint& initialOffset, const FloatPoint& initialVelocity, const FloatSize& initialDelta)
 {
-    for (int i = 0; i < m_numWheelDeltasTracked; ++i)
-        m_wheelDeltaWindow[i] = 0;
+    ASSERT(state == ScrollSnapState::Snapping || state == ScrollSnapState::Gliding);
+    if (m_currentState == state)
+        return;
 
-    m_numWheelDeltasTracked = 0;
+    float targetOffsetX = targetOffsetForStartOffset(ScrollEventAxis::Horizontal, contentSize.width() - viewportSize.width(), initialOffset.x(), pageScale, initialDelta.width(), m_activeSnapIndexX);
+    float targetOffsetY = targetOffsetForStartOffset(ScrollEventAxis::Vertical, contentSize.height() - viewportSize.height(), initialOffset.y(), pageScale, initialDelta.height(), m_activeSnapIndexY);
+    m_momentumCalculator = ScrollingMomentumCalculator::create(viewportSize, contentSize, initialOffset, FloatPoint(targetOffsetX, targetOffsetY), initialDelta, initialVelocity);
+    m_startTime = monotonicallyIncreasingTime();
+    m_currentState = state;
 }
 
-bool ScrollSnapAnimatorState::isSnapping() const
-{
-    return m_currentState == ScrollSnapState::Gliding || m_currentState == ScrollSnapState::Snapping;
-}
-    
-bool ScrollSnapAnimatorState::canReachTargetWithCurrentInitialScrollDelta() const
-{
-    if (m_initialOffset == m_targetOffset || !m_initialScrollDelta)
-        return true;
-    
-    return m_initialOffset < m_targetOffset ? m_initialScrollDelta > 0 : m_initialScrollDelta < 0;
-}
-    
-bool ScrollSnapAnimatorState::wheelDeltaTrackingIsInProgress() const
+void ScrollSnapAnimatorState::transitionToUserInteractionState()
 {
-    return m_numWheelDeltasTracked && m_numWheelDeltasTracked < wheelDeltaWindowSize;
+    teardownAnimationForState(ScrollSnapState::UserInteraction);
 }
 
-bool ScrollSnapAnimatorState::hasFinishedTrackingWheelDeltas() const
+void ScrollSnapAnimatorState::transitionToDestinationReachedState()
 {
-    return m_numWheelDeltasTracked == wheelDeltaWindowSize;
+    teardownAnimationForState(ScrollSnapState::DestinationReached);
 }
 
-float ScrollSnapAnimatorState::interpolatedOffsetAtProgress(float progress) const
+void ScrollSnapAnimatorState::teardownAnimationForState(ScrollSnapState state)
 {
-    progress = std::max(0.0f, std::min(1.0f, progress));
-    return m_initialOffset + progress * (m_targetOffset - m_initialOffset);
+    ASSERT(state == ScrollSnapState::UserInteraction || state == ScrollSnapState::DestinationReached);
+    if (m_currentState == state)
+        return;
+
+    m_momentumCalculator = nullptr;
+    m_startTime = 0;
+    m_currentState = state;
 }
-    
-static const int maxNumScrollSnapParameterEstimationIterations = 10;
-static const float scrollSnapDecayFactorConvergenceThreshold = 0.001;
-static const float initialScrollSnapCurveMagnitude = 1.1;
-static const float minScrollSnapInitialProgress = 0.15;
-static const float maxScrollSnapInitialProgress = 0.5;
-static const double scrollSnapAnimationDuration = 0.5;
-
-/**
- * Computes and sets parameters required for tracking the progress of a snap animation curve, interpolated
- * or linear. The progress curve s(t) maps time t to progress s; both variables are in the interval [0, 1].
- * The time input t is 0 when the current time is the start of the animation, t = m_startTime, and 1 when the
- * current time is at or after the end of the animation, t = m_startTime + m_scrollSnapAnimationDuration.
- *
- * In this exponential progress model, s(t) = A - A * b^(-kt), where k = 60T is the number of frames in the
- * animation (assuming 60 FPS and an animation duration of T) and A, b are reals greater than or equal to 1.
- * Also note that we are given the initial progress, a value indicating the portion of the curve which our
- * initial scroll delta takes us. This is important when matching the initial speed of the animation to the
- * user's initial momentum scrolling speed. Let this initial progress amount equal v_0. I clamp this initial
- * progress amount to a minimum or maximum value.
- *
- * A is referred to as the curve magnitude, while b is referred to as the decay factor. We solve for A and b,
- * keeping the following constraints in mind:
- *     1. s(0) = 0
- *     2. s(1) = 1
- *     3. s(1/k) = v_0
- *
- * First, observe that s(0) = 0 holds for appropriate values of A, b. Solving for the remaining constraints
- * yields a nonlinear system of two equations. In lieu of a purely analytical solution, an alternating
- * optimization scheme is used to approximate A and b. This technique converges quickly (within 5 iterations
- * or so) for appropriate values of v_0. The optimization terminates early when the decay factor changes by
- * less than a threshold between one iteration and the next.
- */
-void ScrollSnapAnimationCurveState::initializeSnapProgressCurve(const FloatSize& initialVector, const FloatSize& targetVector, const FloatSize& initialDelta)
+
+FloatPoint ScrollSnapAnimatorState::currentAnimatedScrollOffset(bool& isAnimationComplete) const
 {
-    float initialProgress = std::max(minScrollSnapInitialProgress, std::min(initialDelta.diagonalLength() / (targetVector - initialVector).diagonalLength(), maxScrollSnapInitialProgress));
-    float previousDecayFactor = 1.0f;
-    m_snapAnimationCurveMagnitude = initialScrollSnapCurveMagnitude;
-    for (int i = 0; i < maxNumScrollSnapParameterEstimationIterations; ++i) {
-        m_snapAnimationDecayFactor = m_snapAnimationCurveMagnitude / (m_snapAnimationCurveMagnitude - initialProgress);
-        m_snapAnimationCurveMagnitude = 1.0f / (1.0f - std::pow(m_snapAnimationDecayFactor, -60.0f * scrollSnapAnimationDuration));
-        if (std::abs(m_snapAnimationDecayFactor - previousDecayFactor) < scrollSnapDecayFactorConvergenceThreshold)
-            break;
-        
-        previousDecayFactor = m_snapAnimationDecayFactor;
+    if (!m_momentumCalculator) {
+        isAnimationComplete = true;
+        return { };
     }
-    m_startTime = monotonicallyIncreasingTime();
-}
 
-/**
- * Computes and sets coefficients required for interpolated snapping when scrolling in 2 dimensions, given
- * initial conditions (the initial and target vectors, along with the initial wheel delta as a vector). The
- * path is a cubic Bezier curve of the form p(s) = INITIAL + (C_1 * s) + (C_2 * s^2) + (C_3 * s^3) where each
- * C_i is a 2D vector and INITIAL is the vector representing the initial scroll offset. s is a real in the
- * interval [0, 1] indicating the "progress" of the curve (i.e. how much of the curve has been traveled).
- *
- * The curve has 4 control points, the first and last of which are the initial and target points, respectively.
- * The distances between adjacent control points are constrained to be the same, making the convex hull an
- * isosceles trapezoid with 3 sides of equal length. Additionally, the vector from the first control point to
- * the second points in the same direction as the initial scroll delta. These constraints ensure two properties:
- *     1. The direction of the snap animation at s=0 will be equal to the direction of the initial scroll delta.
- *     2. Points at regular intervals of s will be evenly spread out.
- *
- * If the initial scroll direction is orthogonal to or points in the opposite direction as the vector from the
- * initial point to the target point, initialization returns early and sets the curve to animate directly to the
- * snap point without interpolation.
- */
-void ScrollSnapAnimationCurveState::initializeInterpolationCoefficientsIfNecessary(const FloatSize& initialVector, const FloatSize& targetVector, const FloatSize& initialDelta)
-{
-    FloatSize startToEndVector = targetVector - initialVector;
-    float startToEndDistance = startToEndVector.diagonalLength();
-    float initialDeltaMagnitude = initialDelta.diagonalLength();
-    float cosTheta = initialDelta.isZero() ? 0 : (initialDelta.width() * startToEndVector.width() + initialDelta.height() * startToEndVector.height()) / (std::max(1.0f, initialDeltaMagnitude) * startToEndDistance);
-    if (cosTheta <= 0)
-        return;
-    
-    float sideLength = startToEndDistance / (2.0f * cosTheta + 1.0f);
-    FloatSize controlVector1 = initialVector + sideLength * initialDelta / initialDeltaMagnitude;
-    FloatSize controlVector2 = controlVector1 + (sideLength * startToEndVector / startToEndDistance);
-    m_snapAnimationCurveCoefficients[0] = initialVector;
-    m_snapAnimationCurveCoefficients[1] = 3 * (controlVector1 - initialVector);
-    m_snapAnimationCurveCoefficients[2] = 3 * (initialVector - 2 * controlVector1 + controlVector2);
-    m_snapAnimationCurveCoefficients[3] = 3 * (controlVector1 - controlVector2) - initialVector + targetVector;
-    shouldAnimateDirectlyToSnapPoint = false;
-}
-    
-FloatPoint ScrollSnapAnimationCurveState::interpolatedPositionAtProgress(float progress) const
-{
-    ASSERT(!shouldAnimateDirectlyToSnapPoint);
-    progress = std::max(0.0f, std::min(1.0f, progress));
-    FloatPoint interpolatedPoint(0.0f, 0.0f);
-    for (int i = 0; i < 4; ++i)
-        interpolatedPoint += std::pow(progress, i) * m_snapAnimationCurveCoefficients[i];
-    
-    return interpolatedPoint;
-}
-    
-bool ScrollSnapAnimationCurveState::shouldCompleteSnapAnimationImmediatelyAtTime(double time) const
-{
-    return m_startTime + scrollSnapAnimationDuration < time;
+    double elapsedTime = monotonicallyIncreasingTime() - m_startTime;
+    isAnimationComplete = elapsedTime >= m_momentumCalculator->animationDuration();
+    return m_momentumCalculator->scrollOffsetAfterElapsedTime(elapsedTime);
 }
 
-float ScrollSnapAnimationCurveState::animationProgressAtTime(double time) const
+float ScrollSnapAnimatorState::targetOffsetForStartOffset(ScrollEventAxis axis, float maxScrollOffset, float startOffset, float pageScale, float initialDelta, unsigned& outActiveSnapIndex) const
 {
-    float timeProgress = std::max(0.0, std::min(1.0, (time - m_startTime) / scrollSnapAnimationDuration));
-    return std::min(1.0, m_snapAnimationCurveMagnitude * (1.0 - std::pow(m_snapAnimationDecayFactor, -60.0f * scrollSnapAnimationDuration * timeProgress)));
+    auto snapOffsets = snapOffsetsForAxis(axis);
+    if (!snapOffsets.size()) {
+        outActiveSnapIndex = 0;
+        return clampTo<float>(startOffset, 0, maxScrollOffset);
+    }
+
+    float projectedDestination = (startOffset + projectedInertialScrollDistance(initialDelta)) / pageScale;
+    float targetOffset = closestSnapOffset<LayoutUnit, float>(snapOffsets, projectedDestination, initialDelta, outActiveSnapIndex);
+    targetOffset = clampTo<float>(targetOffset, snapOffsets.first(), snapOffsets.last());
+    targetOffset = clampTo<float>(targetOffset, 0, maxScrollOffset);
+    return pageScale * targetOffset;
 }
     
 } // namespace WebCore
diff --git a/Source/WebCore/platform/spi/mac/NSScrollingMomentumCalculatorSPI.h b/Source/WebCore/platform/spi/mac/NSScrollingMomentumCalculatorSPI.h
new file mode 100644 (file)
index 0000000..5e89003
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2016 Apple Inc.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#if HAVE(NSSCROLLING_FILTERS)
+
+#if USE(APPLE_INTERNAL_SDK)
+
+#import <AppKit/NSScrollingMomentumCalculator_Private.h>
+
+#else
+
+@interface _NSScrollingMomentumCalculator : NSObject;
+
+- (instancetype)initWithInitialOrigin:(NSPoint)origin velocity:(NSPoint)velocity documentFrame:(NSRect)docFrame constrainedClippingOrigin:(NSPoint)constrainedClippingOrigin clippingSize:(NSSize)clipViewSize tolerance:(NSSize)tolerance;
+- (NSPoint)positionAfterDuration:(NSTimeInterval)duration;
+
+@property (atomic) NSPoint destinationOrigin;
+@property (readonly) NSTimeInterval durationUntilStop;
+
+@end
+
+#endif /* USE(APPLE_INTERNAL_SDK) */
+
+#endif /* HAVE(NSSCROLLING_FILTERS) */
index 9724019..68eff7d 100644 (file)
@@ -1,3 +1,17 @@
+2016-11-29  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        Scroll snapping on Mac should use AppKit animations
+        https://bugs.webkit.org/show_bug.cgi?id=147261
+        <rdar://problem/29395293>
+
+        Reviewed by Brent Fulgham.
+
+        Add some logic to plumb filtered wheel velocity over to WebCore in the case of mainframe scrolling. See
+        WebCore/ChangeLog for more details.
+
+        * WebProcess/WebPage/EventDispatcher.cpp:
+        (WebKit::EventDispatcher::wheelEvent):
+
 2016-11-21  Brian Burg  <bburg@apple.com>
 
         Web Automation: add ObjC SPI to set whether a page is controlled by automation
index 1f29a01..1f22265 100644 (file)
@@ -107,7 +107,7 @@ void EventDispatcher::wheelEvent(uint64_t pageID, const WebWheelEvent& wheelEven
     if (m_recentWheelEventDeltaFilter->isFilteringDeltas()) {
         m_recentWheelEventDeltaFilter->updateFromDelta(FloatSize(platformWheelEvent.deltaX(), platformWheelEvent.deltaY()));
         FloatSize filteredDelta = m_recentWheelEventDeltaFilter->filteredDelta();
-        platformWheelEvent = platformWheelEvent.copyWithDeltas(filteredDelta.width(), filteredDelta.height());
+        platformWheelEvent = platformWheelEvent.copyWithDeltasAndVelocity(filteredDelta.width(), filteredDelta.height(), m_recentWheelEventDeltaFilter->filteredVelocity());
     }
 #endif