Implement "proximity" scroll snapping
authorwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 10 Jan 2017 22:26:56 +0000 (22:26 +0000)
committerwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 10 Jan 2017 22:26:56 +0000 (22:26 +0000)
https://bugs.webkit.org/show_bug.cgi?id=135994
<rdar://problem/18162418>

Reviewed by Dean Jackson.

Source/WebCore:

Adds support for proximity scroll snapping. To do this, we introduce scroll offset ranges, a list of scroll
offset ranges that are plumbed alongside the list of scroll snap offsets. Similar to a snap offset, a snap
offset range contains scroll offsets on which scrolling is allowed to come to a rest within a scroll snapping
container. However, unlike normal snap offsets, scrolling may only come to rest within a snap offset range if
the predicted scroll offset already lies within the range. The new algorithm for selecting a target scroll snap
position given a destination offset is now:

-   If the scroll destination lies within a snap offset range, return the scroll destination
-   Otherwise, compute the nearest lower/upper snap offsets and lower/upper snap offset ranges
-   If scrolling ended with no velocity, return the nearest snap offset
-   If scrolling ended with positive velocity, choose the upper snap offset only if there is no snap offset
    range in between the scroll destination and the snap offset; else, choose the lower snap offset
-   If scrolling ended with negative velocity, choose the lower snap offset only if there is no snap offset
    range in between the scroll destination and the snap offset; else, choose the upper snap offset

The extra rule accounting for scroll offset ranges in between the scroll destination and a potential snap offset
handles the corner case where the user scrolls with momentum very lightly away from a snap offset, such that the
predicted scroll destination is still within proximity of the snap offset. In this case, the regular (mandatory
scroll snapping) behavior would be to snap to the next offset in the direction of momentum scrolling, but
instead, it is more intuitive to return to the original snap position.

We also move scrolling prediction logic into ScrollingMomentumCalculator and adopt the platform
_NSScrollingMomentumCalculator's destinationOrigin property when computing the predicted scroll destination.
Previously, we were simply multiplying by an empirically-derived constant to approximate the scroll destination,
but now that we are supporting proximity scroll snapping, we need more exact scroll destinaton prediction in
order to make sure that scrolling to a snap offset range feels natural.

Tests: tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-then-proximity.html
       tiled-drawing/scrolling/scroll-snap/scroll-snap-proximity-mainframe.html
       tiled-drawing/scrolling/scroll-snap/scroll-snap-proximity-overflow.html

* WebCore.xcodeproj/project.pbxproj:
* page/scrolling/AsyncScrollingCoordinator.cpp:
(WebCore::setStateScrollingNodeSnapOffsetsAsFloat):
(WebCore::AsyncScrollingCoordinator::updateOverflowScrollingNode):
(WebCore::AsyncScrollingCoordinator::updateScrollSnapPropertiesWithFrameView):

Make boilerplate changes to plumb lists of horizontal and vertical snap offset ranges alongside the lists of
horizontal and vertical snap offsets.

* page/scrolling/AxisScrollSnapOffsets.cpp:
(WebCore::snapOffsetRangesToString):
(WebCore::indicesOfNearestSnapOffsetRanges):
(WebCore::indicesOfNearestSnapOffsets):
(WebCore::adjustAxisSnapOffsetsForScrollExtent):
(WebCore::computeAxisProximitySnapOffsetRanges):
(WebCore::updateSnapOffsetsForScrollableArea):
(WebCore::closestSnapOffset):

Adjust the snap offset selection algorithm to take snap offset ranges into account. See above for more details.
Additionally, augment snap offset update logic to emit snap offset ranges for proximity scroll snapping. To do
this, we run the following steps on the final list of processed snap offsets:
-   Compute the proximity distance, which (for now) is arbitrarily 0.3 * the length or width of the scroll snap
    port, depending on whether scroll snapping is taking place in the X or Y axis.
-   For each pair of adjacent snap offsets, if they are more than 2 * proximity distance away from each other,
    emit a snap offset range starting from (lower snap offset + proximity distance) and ending on (upper snap
    offset + proximity distance).

* page/scrolling/AxisScrollSnapOffsets.h:
(WebCore::closestSnapOffset): Deleted.
* page/scrolling/ScrollSnapOffsetsInfo.h:

Introduce ScrollSnapOffsetsInfo, a struct which contains data relevant to scroll snapping. This includes
vertical and horizontal snap offsets, as well as vertical and horizontal snap offset ranges. Snap offset ranges
consist of a vector of ranges of scroll offsets.

* page/scrolling/ScrollingCoordinator.h:
* page/scrolling/ScrollingMomentumCalculator.cpp:
(WebCore::projectedInertialScrollDistance):
(WebCore::ScrollingMomentumCalculator::ScrollingMomentumCalculator):
(WebCore::ScrollingMomentumCalculator::setRetargetedScrollOffset):
(WebCore::ScrollingMomentumCalculator::predictedDestinationOffset):
(WebCore::ScrollingMomentumCalculator::create):
(WebCore::ScrollingMomentumCalculator::setPlatformMomentumScrollingPredictionEnabled):
(WebCore::BasicScrollingMomentumCalculator::BasicScrollingMomentumCalculator):
(WebCore::BasicScrollingMomentumCalculator::linearlyInterpolatedOffsetAtProgress):
(WebCore::BasicScrollingMomentumCalculator::initializeInterpolationCoefficientsIfNecessary):
(WebCore::BasicScrollingMomentumCalculator::initializeSnapProgressCurve):
* page/scrolling/ScrollingMomentumCalculator.h:
(WebCore::ScrollingMomentumCalculator::retargetedScrollOffset):
(WebCore::ScrollingMomentumCalculator::retargetedScrollOffsetDidChange):

Currently, the ScrollingMomentumCalculator is responsible for taking an initial position, initial velocity, and
target position and animating the scroll offset from the initial to target position. Now, we refactor the
ScrollingMomentumCalculator interface to no longer take a target offset upon initialization, and instead compute
the predicted scroll destination given initial position and velocity; clients of the ScrollingMomentumCalculator
then use this predicted scroll destination to compute a retargeted scroll offset and then call
setRetargetedScrollOffset on the calculator, which sets up an animation curve to the new retargeted offset. This
allows both the AppKit-based scrolling momentum calculator and platform-invariant momentum calculator to be used
interchangeably, while still allowing them to compute a destination offset from initial parameters of the
scroll.

* page/scrolling/ScrollingStateScrollingNode.cpp:
(WebCore::ScrollingStateScrollingNode::ScrollingStateScrollingNode):
(WebCore::ScrollingStateScrollingNode::setHorizontalSnapOffsets):
(WebCore::ScrollingStateScrollingNode::setVerticalSnapOffsets):
(WebCore::ScrollingStateScrollingNode::setHorizontalSnapOffsetRanges):
(WebCore::ScrollingStateScrollingNode::setVerticalSnapOffsetRanges):
* page/scrolling/ScrollingStateScrollingNode.h:
(WebCore::ScrollingStateScrollingNode::horizontalSnapOffsets):
(WebCore::ScrollingStateScrollingNode::verticalSnapOffsets):
(WebCore::ScrollingStateScrollingNode::horizontalSnapOffsetRanges):
(WebCore::ScrollingStateScrollingNode::verticalSnapOffsetRanges):
* page/scrolling/ScrollingTreeScrollingNode.cpp:
(WebCore::ScrollingTreeScrollingNode::commitStateBeforeChildren):
(WebCore::ScrollingTreeScrollingNode::dumpProperties):
* page/scrolling/ScrollingTreeScrollingNode.h:
(WebCore::ScrollingTreeScrollingNode::horizontalSnapOffsets):
(WebCore::ScrollingTreeScrollingNode::verticalSnapOffsets):
(WebCore::ScrollingTreeScrollingNode::horizontalSnapOffsetRanges):
(WebCore::ScrollingTreeScrollingNode::verticalSnapOffsetRanges):

Add more boilerplate support for snap offset ranges.

* page/scrolling/mac/ScrollingMomentumCalculatorMac.h:
* page/scrolling/mac/ScrollingMomentumCalculatorMac.mm:
(WebCore::ScrollingMomentumCalculator::create):
(WebCore::ScrollingMomentumCalculator::setPlatformMomentumScrollingPredictionEnabled):
(WebCore::ScrollingMomentumCalculatorMac::ScrollingMomentumCalculatorMac):
(WebCore::ScrollingMomentumCalculatorMac::scrollOffsetAfterElapsedTime):
(WebCore::ScrollingMomentumCalculatorMac::predictedDestinationOffset):
(WebCore::ScrollingMomentumCalculatorMac::retargetedScrollOffsetDidChange):
(WebCore::ScrollingMomentumCalculatorMac::animationDuration):
(WebCore::ScrollingMomentumCalculatorMac::requiresMomentumScrolling):
(WebCore::ScrollingMomentumCalculatorMac::ensurePlatformMomentumCalculator):

Hook into AppKit momentum scroll offset prediction.

* page/scrolling/mac/ScrollingTreeFrameScrollingNodeMac.mm:
(WebCore::convertToLayoutUnits):
(WebCore::ScrollingTreeFrameScrollingNodeMac::commitStateBeforeChildren):
* platform/ScrollableArea.cpp:
(WebCore::ScrollableArea::ensureSnapOffsetsInfo):
(WebCore::ScrollableArea::horizontalSnapOffsets):
(WebCore::ScrollableArea::horizontalSnapOffsetRanges):
(WebCore::ScrollableArea::verticalSnapOffsetRanges):
(WebCore::ScrollableArea::verticalSnapOffsets):
(WebCore::ScrollableArea::setHorizontalSnapOffsets):
(WebCore::ScrollableArea::setVerticalSnapOffsets):
(WebCore::ScrollableArea::setHorizontalSnapOffsetRanges):
(WebCore::ScrollableArea::setVerticalSnapOffsetRanges):
(WebCore::ScrollableArea::clearHorizontalSnapOffsets):
(WebCore::ScrollableArea::clearVerticalSnapOffsets):
* platform/ScrollableArea.h:
(WebCore::ScrollableArea::horizontalSnapOffsets): Deleted.
(WebCore::ScrollableArea::verticalSnapOffsets): Deleted.
* platform/cocoa/ScrollController.h:
* platform/cocoa/ScrollController.mm:
(WebCore::ScrollController::processWheelEventForScrollSnap):

Fix an issue where initial scrolling velocity would be set to zero at the end of a drag gesture.

(WebCore::ScrollController::updateScrollSnapState):
(WebCore::ScrollController::updateScrollSnapPoints):
(WebCore::ScrollController::setNearestScrollSnapIndexForAxisAndOffset):
* platform/cocoa/ScrollSnapAnimatorState.h:
(WebCore::ScrollSnapAnimatorState::snapOffsetsForAxis):
(WebCore::ScrollSnapAnimatorState::snapOffsetRangesForAxis):
(WebCore::ScrollSnapAnimatorState::setSnapOffsetsAndPositionRangesForAxis):
(WebCore::ScrollSnapAnimatorState::setSnapOffsetsForAxis): Deleted.
* platform/cocoa/ScrollSnapAnimatorState.mm:
(WebCore::ScrollSnapAnimatorState::setupAnimationForState):
(WebCore::ScrollSnapAnimatorState::targetOffsetForStartOffset):
(WebCore::projectedInertialScrollDistance): Deleted.
* rendering/RenderLayerCompositor.cpp:
(WebCore::RenderLayerCompositor::updateScrollCoordinatedLayer):
* testing/Internals.cpp:
(WebCore::Internals::setPlatformMomentumScrollingPredictionEnabled):

Add a new hook for layout tests to force scrolling momentum calculators to use the platform-invariant momentum
scrolling prediction heuristic instead of the platform-dependent one.

(WebCore::Internals::scrollSnapOffsets):
* testing/Internals.h:
* testing/Internals.idl:

Source/WebKit2:

Adds boilerplate support for plumbing lists of snap offset ranges from the web process to the UI process
alongside the list of snap offsets.

* Shared/Scrolling/RemoteScrollingCoordinatorTransaction.cpp:
(ArgumentCoder<ScrollingStateScrollingNode>::encode):
(ArgumentCoder<ScrollingStateScrollingNode>::decode):
* Shared/WebCoreArgumentCoders.cpp:
(IPC::ArgumentCoder<ScrollOffsetRange<float>>::encode):
(IPC::ArgumentCoder<ScrollOffsetRange<float>>::decode):
* Shared/WebCoreArgumentCoders.h:
* UIProcess/Scrolling/ios/ScrollingTreeOverflowScrollingNodeIOS.mm:
(-[WKOverflowScrollViewDelegate scrollViewWillEndDragging:withVelocity:targetContentOffset:]):
* UIProcess/ios/RemoteScrollingCoordinatorProxyIOS.mm:

Adjust mainframe proximity scroll snapping logic to not subtract out the top content inset when there is no
active snap offset (i.e. when snapping rests in a snap offset range). Attempting to subtract out the top inset
in this case caused the scroll offset to jump after ending a drag with no momentum in a snap offset range.

(WebKit::RemoteScrollingCoordinatorProxy::adjustTargetContentOffsetForSnapping):
(WebKit::RemoteScrollingCoordinatorProxy::shouldSnapForMainFrameScrolling):
(WebKit::RemoteScrollingCoordinatorProxy::closestSnapOffsetForMainFrameScrolling):

LayoutTests:

Adds 3 new layout tests for proximity scroll snapping. Also tweaks some existing tests that test scroll snapping
after scrolling with momentum to use the custom heuristic for predicting scroll destination instead of platform
momentum scrolling. This ensures that the results of our layout tests that depend on predicting momentum scroll
destination are consistent across runs.

* tiled-drawing/scrolling/latched-div-with-scroll-snap.html:
* tiled-drawing/scrolling/scroll-snap/scroll-snap-iframe.html:
* tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-2d-overflow.html:
* tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-borders.html:
* tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-hidden-scrollbars.html:
* tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-horizontal.html:
* 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.html:
* tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-vertical.html:
* tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-overflow-stateless.html:
* tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-overflow.html:
* tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-padding.html:
* tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-rotated.html:

Force these tests to use platform-independent scrolling momentum prediction, by multiplying the last scroll
delta upon release by a constant factor.

* tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-then-proximity-expected.txt: Added.
* tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-then-proximity.html: Added.

Tests that after changing scroll-snap-type from mandatory to proximity, swiping downwards no longer snaps the
scroll offset to the second box, but instead leaves the scroll offset somewhere in the middle of the first box.

* tiled-drawing/scrolling/scroll-snap/scroll-snap-proximity-mainframe-expected.txt: Added.
* tiled-drawing/scrolling/scroll-snap/scroll-snap-proximity-mainframe.html: Added.

Tests that when scroll-snap-type is proximity in the mainframe, scrolling slightly downwards snaps the scroll
offset back up to the top; scrolling somewhere in the middle of the first box does not snap the scroll offset;
and scrolling near the end of the first box snaps the scroll offset to the second box.

* tiled-drawing/scrolling/scroll-snap/scroll-snap-proximity-overflow-expected.txt: Added.
* tiled-drawing/scrolling/scroll-snap/scroll-snap-proximity-overflow.html: Added.

Similar to scroll-snap-proximity-mainframe.html, except for overflow scrolling instead of the mainframe.

* tiled-drawing/scrolling/scroll-snap/scroll-snap-scrolling-jumps-to-top.html:

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

54 files changed:
LayoutTests/ChangeLog
LayoutTests/tiled-drawing/scrolling/latched-div-with-scroll-snap.html
LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-iframe.html
LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-2d-overflow.html
LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-borders.html
LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-hidden-scrollbars.html
LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-horizontal.html
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.html
LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-vertical.html
LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-overflow-stateless.html
LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-overflow.html
LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-padding.html
LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-rotated.html
LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-then-proximity-expected.txt [new file with mode: 0644]
LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-then-proximity.html [new file with mode: 0644]
LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-proximity-mainframe-expected.txt [new file with mode: 0644]
LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-proximity-mainframe.html [new file with mode: 0644]
LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-proximity-overflow-expected.txt [new file with mode: 0644]
LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-proximity-overflow.html [new file with mode: 0644]
LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-scrolling-jumps-to-top.html
Source/WebCore/ChangeLog
Source/WebCore/WebCore.xcodeproj/project.pbxproj
Source/WebCore/page/scrolling/AsyncScrollingCoordinator.cpp
Source/WebCore/page/scrolling/AxisScrollSnapOffsets.cpp
Source/WebCore/page/scrolling/AxisScrollSnapOffsets.h
Source/WebCore/page/scrolling/ScrollSnapOffsetsInfo.h [new file with mode: 0644]
Source/WebCore/page/scrolling/ScrollingCoordinator.h
Source/WebCore/page/scrolling/ScrollingMomentumCalculator.cpp
Source/WebCore/page/scrolling/ScrollingMomentumCalculator.h
Source/WebCore/page/scrolling/ScrollingStateScrollingNode.cpp
Source/WebCore/page/scrolling/ScrollingStateScrollingNode.h
Source/WebCore/page/scrolling/ScrollingTreeScrollingNode.cpp
Source/WebCore/page/scrolling/ScrollingTreeScrollingNode.h
Source/WebCore/page/scrolling/mac/ScrollingMomentumCalculatorMac.h
Source/WebCore/page/scrolling/mac/ScrollingMomentumCalculatorMac.mm
Source/WebCore/page/scrolling/mac/ScrollingTreeFrameScrollingNodeMac.mm
Source/WebCore/platform/ScrollableArea.cpp
Source/WebCore/platform/ScrollableArea.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/rendering/RenderLayerCompositor.cpp
Source/WebCore/testing/Internals.cpp
Source/WebCore/testing/Internals.h
Source/WebCore/testing/Internals.idl
Source/WebKit2/ChangeLog
Source/WebKit2/Shared/Scrolling/RemoteScrollingCoordinatorTransaction.cpp
Source/WebKit2/Shared/WebCoreArgumentCoders.cpp
Source/WebKit2/Shared/WebCoreArgumentCoders.h
Source/WebKit2/UIProcess/Scrolling/ios/ScrollingTreeOverflowScrollingNodeIOS.mm
Source/WebKit2/UIProcess/ios/RemoteScrollingCoordinatorProxyIOS.mm

index 19c223b..fb23811 100644 (file)
@@ -1,3 +1,54 @@
+2017-01-10  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        Implement "proximity" scroll snapping
+        https://bugs.webkit.org/show_bug.cgi?id=135994
+        <rdar://problem/18162418>
+
+        Reviewed by Dean Jackson.
+
+        Adds 3 new layout tests for proximity scroll snapping. Also tweaks some existing tests that test scroll snapping
+        after scrolling with momentum to use the custom heuristic for predicting scroll destination instead of platform
+        momentum scrolling. This ensures that the results of our layout tests that depend on predicting momentum scroll
+        destination are consistent across runs.
+
+        * tiled-drawing/scrolling/latched-div-with-scroll-snap.html:
+        * tiled-drawing/scrolling/scroll-snap/scroll-snap-iframe.html:
+        * tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-2d-overflow.html:
+        * tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-borders.html:
+        * tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-hidden-scrollbars.html:
+        * tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-horizontal.html:
+        * 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.html:
+        * tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-mainframe-vertical.html:
+        * tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-overflow-stateless.html:
+        * tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-overflow.html:
+        * tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-padding.html:
+        * tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-rotated.html:
+
+        Force these tests to use platform-independent scrolling momentum prediction, by multiplying the last scroll
+        delta upon release by a constant factor.
+
+        * tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-then-proximity-expected.txt: Added.
+        * tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-then-proximity.html: Added.
+
+        Tests that after changing scroll-snap-type from mandatory to proximity, swiping downwards no longer snaps the
+        scroll offset to the second box, but instead leaves the scroll offset somewhere in the middle of the first box.
+
+        * tiled-drawing/scrolling/scroll-snap/scroll-snap-proximity-mainframe-expected.txt: Added.
+        * tiled-drawing/scrolling/scroll-snap/scroll-snap-proximity-mainframe.html: Added.
+
+        Tests that when scroll-snap-type is proximity in the mainframe, scrolling slightly downwards snaps the scroll
+        offset back up to the top; scrolling somewhere in the middle of the first box does not snap the scroll offset;
+        and scrolling near the end of the first box snaps the scroll offset to the second box.
+
+        * tiled-drawing/scrolling/scroll-snap/scroll-snap-proximity-overflow-expected.txt: Added.
+        * tiled-drawing/scrolling/scroll-snap/scroll-snap-proximity-overflow.html: Added.
+
+        Similar to scroll-snap-proximity-mainframe.html, except for overflow scrolling instead of the mainframe.
+
+        * tiled-drawing/scrolling/scroll-snap/scroll-snap-scrolling-jumps-to-top.html:
+
 2017-01-10  Chris Dumez  <cdumez@apple.com>
 
         Make Event.initEvent()'s first parameter mandatory
index 4735aea..d9af7e5 100644 (file)
@@ -74,6 +74,7 @@
             return new Promise(resolve => {
                 write(`* Swiping ${momentum ? "with" : "without"} momentum in ${element.id} with scroll offset ${element.scrollLeft}`);
                 let location = locationInWindowCoordinates(element);
+                internals.setPlatformMomentumScrollingPredictionEnabled(false);
                 eventSender.monitorWheelEvents();
                 eventSender.mouseMoveTo(location.x, location.y);
                 eventSender.mouseScrollByWithWheelAndMomentumPhases(-1, 0, "began", "none");
index 182e19f..df2e1ff 100644 (file)
         {
             if (window.eventSender) {
                 eventSender.monitorWheelEvents();
+                internals.setPlatformMomentumScrollingPredictionEnabled(false);
                 setTimeout(function() { scrollGlideTest('horizontalTarget') }, 0);
             }
         }
index ac1ab88..7b0ad3b 100644 (file)
         function onLoad() {
             if (window.eventSender) {
                 eventSender.monitorWheelEvents();
+                internals.setPlatformMomentumScrollingPredictionEnabled(false);
                 setTimeout(scrollGlideTest, 0);
             } else {
                 var messageLocation = document.getElementById("snap-from");
index 41ded09..9c75258 100644 (file)
         {
             if (window.eventSender) {
                 eventSender.monitorWheelEvents();
+                internals.setPlatformMomentumScrollingPredictionEnabled(false);
                 setTimeout(function() { scrollGlideTest('horizontalTarget') }, 0);
             } else {
                 var messageLocationH = document.getElementById('itemH0');
index b4cf898..a043590 100644 (file)
@@ -44,6 +44,7 @@
                 return;
             }
 
+            internals.setPlatformMomentumScrollingPredictionEnabled(false);
             eventSender.monitorWheelEvents();
             eventSender.mouseMoveTo(250, 250);
             eventSender.mouseScrollByWithWheelAndMomentumPhases(0, -1, "began", "none");
index 61ea2ec..f9d7278 100644 (file)
@@ -91,6 +91,7 @@
 
             if (window.eventSender) {
                 eventSender.monitorWheelEvents();
+                internals.setPlatformMomentumScrollingPredictionEnabled(false);
                 setTimeout(scrollGlideTest, 0);
             } else {
                 var messageLocation = document.getElementById('item0');
index c5cd03c..dfdde85 100644 (file)
@@ -91,6 +91,7 @@
 
             if (window.eventSender) {
                 eventSender.monitorWheelEvents();
+                internals.setPlatformMomentumScrollingPredictionEnabled(false);
                 setTimeout(scrollGlideTest, 0);
             } else {
                 var messageLocation = document.getElementById('item0');
index 8d85147..6268244 100644 (file)
@@ -90,6 +90,7 @@
         function onLoad() {
             if (window.eventSender) {
                 eventSender.monitorWheelEvents();
+                internals.setPlatformMomentumScrollingPredictionEnabled(false);
                 setTimeout(scrollGlideTest, 0);
             } else {
                 var messageLocation = document.getElementById('item0');
index 89f4e18..8538581 100644 (file)
@@ -52,6 +52,7 @@
         function onLoad() {
             if (window.eventSender) {
                 eventSender.monitorWheelEvents();
+                internals.setPlatformMomentumScrollingPredictionEnabled(false);
                 setTimeout(scrollSnapTest, 0);
             } else {
                 var messageLocation = document.getElementById('item0');
index 6e71013..9fd289c 100644 (file)
@@ -91,6 +91,7 @@
 
             if (window.eventSender) {
                 eventSender.monitorWheelEvents();
+                internals.setPlatformMomentumScrollingPredictionEnabled(false);
                 setTimeout(scrollGlideTest, 0);
             } else {
                 var messageLocation = document.getElementById('item0');
index cb94f69..d25b59b 100644 (file)
@@ -68,6 +68,7 @@
         {
             if (window.eventSender) {
                 eventSender.monitorWheelEvents();
+                internals.setPlatformMomentumScrollingPredictionEnabled(false);
                 setTimeout(scrollSnapTest, 0);
             } else {
                 var messageLocationH = document.getElementById("item0");
index b36fdd5..f732f3a 100644 (file)
         {
             if (window.eventSender) {
                 eventSender.monitorWheelEvents();
+                internals.setPlatformMomentumScrollingPredictionEnabled(false);
                 setTimeout(function() { scrollGlideTest('horizontalTarget') }, 0);
             } else {
                 var messageLocationH = document.getElementById('itemH0');
index 0a117dc..5dbc1cf 100644 (file)
         {
             if (window.eventSender) {
                 eventSender.monitorWheelEvents();
+                internals.setPlatformMomentumScrollingPredictionEnabled(false);
                 setTimeout(function() { scrollGlideTest('horizontalTarget') }, 0);
             } else {
                 var messageLocationH = document.getElementById('itemH0');
index c9301ad..c4ee947 100644 (file)
         {
             if (window.eventSender) {
                 eventSender.monitorWheelEvents();
+                internals.setPlatformMomentumScrollingPredictionEnabled(false);
                 setTimeout(function() { scrollGlideTest('horizontalTarget') }, 0);
             } else {
                 var messageLocationH = document.getElementById('itemH0');
diff --git a/LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-then-proximity-expected.txt b/LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-then-proximity-expected.txt
new file mode 100644 (file)
index 0000000..aa03f13
--- /dev/null
@@ -0,0 +1,7 @@
+Scrolling in container with scroll-snap-type: y mandatory
+- Did the scrolling snap to the top? NO
+- Did scrolling snap to the second box? YES
+Scrolling in container with scroll-snap-type: y proximity
+- Did the scrolling snap to the top? NO
+- Did scrolling snap to the second box? NO
+
diff --git a/LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-then-proximity.html b/LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-then-proximity.html
new file mode 100644 (file)
index 0000000..42feded
--- /dev/null
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+    <head>
+        <style>
+            body {
+                margin: 0;
+                overflow: hidden;
+            }
+
+            #container {
+                width: 600px;
+                height: 600px;
+                position: absolute;
+                top: 0;
+                left: 0;
+                overflow-x: none;
+                overflow-y: scroll;
+                scroll-snap-type: y mandatory;
+                opacity: 0.5;
+            }
+
+            .area {
+                height: 600px;
+                width: 600px;
+                float: left;
+                scroll-snap-align: start;
+            }
+        </style>
+        <script>
+        let write = s => output.innerHTML += s + "<br>";
+        if (window.testRunner) {
+            testRunner.dumpAsText();
+            testRunner.waitUntilDone();
+        }
+
+        function verticalScrollInContainer(dragDeltas, momentumDeltas)
+        {
+            return new Promise(resolve => {
+                write(`Scrolling in ${container.id} with scroll-snap-type: ${getComputedStyle(container).scrollSnapType}`);
+                eventSender.monitorWheelEvents();
+                internals.setPlatformMomentumScrollingPredictionEnabled(false);
+                eventSender.mouseMoveTo(300, 300);
+                dragDeltas.forEach((delta, i) => eventSender.mouseScrollByWithWheelAndMomentumPhases(0, delta, i == 0 ? "began" : "changed", "none"));
+                eventSender.mouseScrollByWithWheelAndMomentumPhases(0, 0, "ended", "none");
+                if (momentumDeltas && momentumDeltas.length) {
+                    momentumDeltas.forEach((delta, i) => eventSender.mouseScrollByWithWheelAndMomentumPhases(0, delta, "none", i == 0 ? "begin" : "continue"));
+                    eventSender.mouseScrollByWithWheelAndMomentumPhases(0, 0, "none", "end");
+                }
+                eventSender.callAfterScrollingCompletes(() => {
+                    write(`- Did the scrolling snap to the top? ${container.scrollTop == 0 ? "YES" : "NO"}`);
+                    write(`- Did scrolling snap to the second box? ${container.scrollTop == 600 ? "YES" : "NO"}`);
+                    container.style.scrollSnapType = "y proximity"
+                    container.scrollTop = 0;
+                    setTimeout(resolve, 0);
+                });
+            });
+        }
+
+        function run() {
+            if (!window.testRunner || !window.eventSender) {
+                write("This test requires EventSender support.");
+                return;
+            }
+
+            verticalScrollInContainer(new Array(16).fill(-1), new Array(3).fill(-1))
+                .then(() => verticalScrollInContainer(new Array(16).fill(-1), new Array(3).fill(-1)))
+                .then(() => testRunner.notifyDone());
+        }
+        </script>
+    </head>
+    <body onload=run()>
+        <div id="container">
+            <div class="area" style="background-color: red;"></div>
+            <div class="area" style="background-color: green;"></div>
+            <div class="area" style="background-color: blue;"></div>
+            <div class="area" style="background-color: aqua;"></div>
+            <div class="area" style="background-color: yellow;"></div>
+            <div class="area" style="background-color: fuchsia;"></div>
+        </div>
+        <div id="output"></div>
+    </body>
+</html>
diff --git a/LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-proximity-mainframe-expected.txt b/LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-proximity-mainframe-expected.txt
new file mode 100644 (file)
index 0000000..7d410af
--- /dev/null
@@ -0,0 +1,10 @@
+Scrolling body with 2 drag ticks
+- Did the scrolling snap to the top? YES
+- Did scrolling snap to the second box? NO
+Scrolling body with 30 drag ticks
+- Did the scrolling snap to the top? NO
+- Did scrolling snap to the second box? NO
+Scrolling body with 60 drag ticks
+- Did the scrolling snap to the top? NO
+- Did scrolling snap to the second box? YES
+
diff --git a/LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-proximity-mainframe.html b/LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-proximity-mainframe.html
new file mode 100644 (file)
index 0000000..679971b
--- /dev/null
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+    <head>
+        <style>
+            body {
+                margin: 0;
+                width: 100%;
+                height: 100%;
+                overflow-x: none;
+                overflow-y: scroll;
+                position: absolute;
+                scroll-snap-type: y proximity;
+                -webkit-scroll-snap-type: proximity;
+                scroll-snap-type: proximity;
+            }
+
+            .area {
+                width: 100%;
+                height: 100%;
+                float: left;
+                opacity: 0.5;
+                scroll-snap-align: start;
+                -webkit-scroll-snap-coordinate: 0 0;
+                scroll-snap-coordinate: 0 0;
+            }
+
+            #output {
+                position: fixed;
+            }
+        </style>
+        <script>
+        let write = s => output.innerHTML += s + "<br>";
+        if (window.testRunner) {
+            testRunner.dumpAsText();
+            testRunner.waitUntilDone();
+        }
+
+        function verticalScrollInBody(dragDeltas)
+        {
+            return new Promise(resolve => {
+                write(`Scrolling body with ${dragDeltas.length} drag ticks`);
+                eventSender.monitorWheelEvents();
+                internals.setPlatformMomentumScrollingPredictionEnabled(false);
+                eventSender.mouseMoveTo(window.innerWidth / 2, window.innerHeight / 2);
+                dragDeltas.forEach((delta, i) => eventSender.mouseScrollByWithWheelAndMomentumPhases(0, delta, i == 0 ? "began" : "changed", "none"));
+                eventSender.mouseScrollByWithWheelAndMomentumPhases(0, 0, "ended", "none");
+                eventSender.callAfterScrollingCompletes(() => {
+                    let areaHeight = document.querySelector(".area").clientHeight;
+                    write(`- Did the scrolling snap to the top? ${document.body.scrollTop == 0 ? "YES" : "NO"}`);
+                    write(`- Did scrolling snap to the second box? ${document.body.scrollTop == areaHeight ? "YES" : "NO"}`);
+                    document.body.scrollTop = 0;
+                    setTimeout(resolve, 0);
+                });
+            });
+        }
+
+        function run() {
+            if (!window.testRunner || !window.eventSender) {
+                write("To manually test, verify that scrolling near one of the boundaries between the colored boxes");
+                write("snaps to the edge of the nearest colored box, but scrolling somewhere near the middle of two");
+                write("boxes does not cause the scroll offset to snap.");
+                return;
+            }
+
+            let areaHeight = document.querySelector(".area").clientHeight;
+            verticalScrollInBody(new Array(2).fill(-1))
+                .then(() => verticalScrollInBody(new Array(Math.round(areaHeight / 20)).fill(-1)))
+                .then(() => verticalScrollInBody(new Array(Math.round(areaHeight / 10)).fill(-1)))
+                .then(() => testRunner.notifyDone());
+        }
+        </script>
+    </head>
+    <body onload=run()>
+        <div class="area" style="background-color: red;"></div>
+        <div class="area" style="background-color: green;"></div>
+        <div class="area" style="background-color: blue;"></div>
+        <div class="area" style="background-color: aqua;"></div>
+        <div class="area" style="background-color: yellow;"></div>
+        <div class="area" style="background-color: fuchsia;"></div>
+    </body>
+    <div id="output"></div>
+</html>
diff --git a/LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-proximity-overflow-expected.txt b/LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-proximity-overflow-expected.txt
new file mode 100644 (file)
index 0000000..a59f7a9
--- /dev/null
@@ -0,0 +1,10 @@
+Scrolling in container with 2 drag ticks
+- Did the scrolling snap to the top? YES
+- Did scrolling snap to the second box? NO
+Scrolling in container with 31 drag ticks
+- Did the scrolling snap to the top? NO
+- Did scrolling snap to the second box? NO
+Scrolling in container with 59 drag ticks
+- Did the scrolling snap to the top? NO
+- Did scrolling snap to the second box? YES
+
diff --git a/LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-proximity-overflow.html b/LayoutTests/tiled-drawing/scrolling/scroll-snap/scroll-snap-proximity-overflow.html
new file mode 100644 (file)
index 0000000..5b052fd
--- /dev/null
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+    <head>
+        <style>
+            body {
+                margin: 0;
+                overflow: hidden;
+            }
+
+            #container {
+                width: 600px;
+                height: 600px;
+                position: absolute;
+                top: 0;
+                left: 0;
+                overflow-x: none;
+                overflow-y: scroll;
+                scroll-snap-type: y proximity;
+                -webkit-scroll-snap-type: proximity;
+                scroll-snap-type: proximity;
+                opacity: 0.5;
+            }
+
+            .area {
+                height: 600px;
+                width: 600px;
+                float: left;
+                scroll-snap-align: start;
+                -webkit-scroll-snap-coordinate: 0 0;
+                scroll-snap-coordinate: 0 0;
+            }
+        </style>
+        <script>
+        let write = s => output.innerHTML += s + "<br>";
+        if (window.testRunner) {
+            testRunner.dumpAsText();
+            testRunner.waitUntilDone();
+        }
+
+        function verticalScrollInContainer(dragDeltas)
+        {
+            return new Promise(resolve => {
+                write(`Scrolling in ${container.id} with ${dragDeltas.length} drag ticks`);
+                eventSender.monitorWheelEvents();
+                internals.setPlatformMomentumScrollingPredictionEnabled(false);
+                eventSender.mouseMoveTo(300, 300);
+                dragDeltas.forEach((delta, i) => eventSender.mouseScrollByWithWheelAndMomentumPhases(0, delta, i == 0 ? "began" : "changed", "none"));
+                eventSender.mouseScrollByWithWheelAndMomentumPhases(0, 0, "ended", "none");
+                eventSender.callAfterScrollingCompletes(() => {
+                    write(`- Did the scrolling snap to the top? ${container.scrollTop == 0 ? "YES" : "NO"}`);
+                    write(`- Did scrolling snap to the second box? ${container.scrollTop == 600 ? "YES" : "NO"}`);
+                    container.scrollTop = 0;
+                    setTimeout(resolve, 0);
+                });
+            });
+        }
+
+        function run() {
+            if (!window.testRunner || !window.eventSender) {
+                write("To manually test, verify that scrolling near one of the boundaries between the colored boxes");
+                write("snaps to the edge of the nearest colored box, but scrolling somewhere near the middle of two");
+                write("boxes does not cause the scroll offset to snap.");
+                return;
+            }
+
+            verticalScrollInContainer(new Array(2).fill(-1))
+                .then(() => verticalScrollInContainer(new Array(31).fill(-1)))
+                .then(() => verticalScrollInContainer(new Array(59).fill(-1)))
+                .then(() => testRunner.notifyDone());
+        }
+        </script>
+    </head>
+    <body onload=run()>
+        <div id="container">
+            <div class="area" style="background-color: red;"></div>
+            <div class="area" style="background-color: green;"></div>
+            <div class="area" style="background-color: blue;"></div>
+            <div class="area" style="background-color: aqua;"></div>
+            <div class="area" style="background-color: yellow;"></div>
+            <div class="area" style="background-color: fuchsia;"></div>
+        </div>
+        <div id="output"></div>
+    </body>
+</html>
index a6bad49..fc9107b 100644 (file)
@@ -34,6 +34,7 @@
             testRunner.dumpAsText();
             testRunner.waitUntilDone();
             eventSender.monitorWheelEvents();
+            internals.setPlatformMomentumScrollingPredictionEnabled(false);
             eventSender.mouseMoveTo(container.offsetLeft + container.clientWidth / 2, container.offsetTop + container.clientHeight / 2);
 
             write("Scrolling without momentum to the same position several times")
index 73c5f65..083d221 100644 (file)
@@ -1,3 +1,187 @@
+2017-01-10  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        Implement "proximity" scroll snapping
+        https://bugs.webkit.org/show_bug.cgi?id=135994
+        <rdar://problem/18162418>
+
+        Reviewed by Dean Jackson.
+
+        Adds support for proximity scroll snapping. To do this, we introduce scroll offset ranges, a list of scroll
+        offset ranges that are plumbed alongside the list of scroll snap offsets. Similar to a snap offset, a snap
+        offset range contains scroll offsets on which scrolling is allowed to come to a rest within a scroll snapping
+        container. However, unlike normal snap offsets, scrolling may only come to rest within a snap offset range if
+        the predicted scroll offset already lies within the range. The new algorithm for selecting a target scroll snap
+        position given a destination offset is now:
+
+        -   If the scroll destination lies within a snap offset range, return the scroll destination
+        -   Otherwise, compute the nearest lower/upper snap offsets and lower/upper snap offset ranges
+        -   If scrolling ended with no velocity, return the nearest snap offset
+        -   If scrolling ended with positive velocity, choose the upper snap offset only if there is no snap offset
+            range in between the scroll destination and the snap offset; else, choose the lower snap offset
+        -   If scrolling ended with negative velocity, choose the lower snap offset only if there is no snap offset
+            range in between the scroll destination and the snap offset; else, choose the upper snap offset
+
+        The extra rule accounting for scroll offset ranges in between the scroll destination and a potential snap offset
+        handles the corner case where the user scrolls with momentum very lightly away from a snap offset, such that the
+        predicted scroll destination is still within proximity of the snap offset. In this case, the regular (mandatory
+        scroll snapping) behavior would be to snap to the next offset in the direction of momentum scrolling, but
+        instead, it is more intuitive to return to the original snap position.
+
+        We also move scrolling prediction logic into ScrollingMomentumCalculator and adopt the platform
+        _NSScrollingMomentumCalculator's destinationOrigin property when computing the predicted scroll destination.
+        Previously, we were simply multiplying by an empirically-derived constant to approximate the scroll destination,
+        but now that we are supporting proximity scroll snapping, we need more exact scroll destinaton prediction in
+        order to make sure that scrolling to a snap offset range feels natural.
+
+        Tests: tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-then-proximity.html
+               tiled-drawing/scrolling/scroll-snap/scroll-snap-proximity-mainframe.html
+               tiled-drawing/scrolling/scroll-snap/scroll-snap-proximity-overflow.html
+
+        * WebCore.xcodeproj/project.pbxproj:
+        * page/scrolling/AsyncScrollingCoordinator.cpp:
+        (WebCore::setStateScrollingNodeSnapOffsetsAsFloat):
+        (WebCore::AsyncScrollingCoordinator::updateOverflowScrollingNode):
+        (WebCore::AsyncScrollingCoordinator::updateScrollSnapPropertiesWithFrameView):
+
+        Make boilerplate changes to plumb lists of horizontal and vertical snap offset ranges alongside the lists of
+        horizontal and vertical snap offsets.
+
+        * page/scrolling/AxisScrollSnapOffsets.cpp:
+        (WebCore::snapOffsetRangesToString):
+        (WebCore::indicesOfNearestSnapOffsetRanges):
+        (WebCore::indicesOfNearestSnapOffsets):
+        (WebCore::adjustAxisSnapOffsetsForScrollExtent):
+        (WebCore::computeAxisProximitySnapOffsetRanges):
+        (WebCore::updateSnapOffsetsForScrollableArea):
+        (WebCore::closestSnapOffset):
+
+        Adjust the snap offset selection algorithm to take snap offset ranges into account. See above for more details.
+        Additionally, augment snap offset update logic to emit snap offset ranges for proximity scroll snapping. To do
+        this, we run the following steps on the final list of processed snap offsets:
+        -   Compute the proximity distance, which (for now) is arbitrarily 0.3 * the length or width of the scroll snap
+            port, depending on whether scroll snapping is taking place in the X or Y axis.
+        -   For each pair of adjacent snap offsets, if they are more than 2 * proximity distance away from each other,
+            emit a snap offset range starting from (lower snap offset + proximity distance) and ending on (upper snap
+            offset + proximity distance).
+
+        * page/scrolling/AxisScrollSnapOffsets.h:
+        (WebCore::closestSnapOffset): Deleted.
+        * page/scrolling/ScrollSnapOffsetsInfo.h:
+
+        Introduce ScrollSnapOffsetsInfo, a struct which contains data relevant to scroll snapping. This includes
+        vertical and horizontal snap offsets, as well as vertical and horizontal snap offset ranges. Snap offset ranges
+        consist of a vector of ranges of scroll offsets.
+
+        * page/scrolling/ScrollingCoordinator.h:
+        * page/scrolling/ScrollingMomentumCalculator.cpp:
+        (WebCore::projectedInertialScrollDistance):
+        (WebCore::ScrollingMomentumCalculator::ScrollingMomentumCalculator):
+        (WebCore::ScrollingMomentumCalculator::setRetargetedScrollOffset):
+        (WebCore::ScrollingMomentumCalculator::predictedDestinationOffset):
+        (WebCore::ScrollingMomentumCalculator::create):
+        (WebCore::ScrollingMomentumCalculator::setPlatformMomentumScrollingPredictionEnabled):
+        (WebCore::BasicScrollingMomentumCalculator::BasicScrollingMomentumCalculator):
+        (WebCore::BasicScrollingMomentumCalculator::linearlyInterpolatedOffsetAtProgress):
+        (WebCore::BasicScrollingMomentumCalculator::initializeInterpolationCoefficientsIfNecessary):
+        (WebCore::BasicScrollingMomentumCalculator::initializeSnapProgressCurve):
+        * page/scrolling/ScrollingMomentumCalculator.h:
+        (WebCore::ScrollingMomentumCalculator::retargetedScrollOffset):
+        (WebCore::ScrollingMomentumCalculator::retargetedScrollOffsetDidChange):
+
+        Currently, the ScrollingMomentumCalculator is responsible for taking an initial position, initial velocity, and
+        target position and animating the scroll offset from the initial to target position. Now, we refactor the
+        ScrollingMomentumCalculator interface to no longer take a target offset upon initialization, and instead compute
+        the predicted scroll destination given initial position and velocity; clients of the ScrollingMomentumCalculator
+        then use this predicted scroll destination to compute a retargeted scroll offset and then call
+        setRetargetedScrollOffset on the calculator, which sets up an animation curve to the new retargeted offset. This
+        allows both the AppKit-based scrolling momentum calculator and platform-invariant momentum calculator to be used
+        interchangeably, while still allowing them to compute a destination offset from initial parameters of the
+        scroll.
+
+        * page/scrolling/ScrollingStateScrollingNode.cpp:
+        (WebCore::ScrollingStateScrollingNode::ScrollingStateScrollingNode):
+        (WebCore::ScrollingStateScrollingNode::setHorizontalSnapOffsets):
+        (WebCore::ScrollingStateScrollingNode::setVerticalSnapOffsets):
+        (WebCore::ScrollingStateScrollingNode::setHorizontalSnapOffsetRanges):
+        (WebCore::ScrollingStateScrollingNode::setVerticalSnapOffsetRanges):
+        * page/scrolling/ScrollingStateScrollingNode.h:
+        (WebCore::ScrollingStateScrollingNode::horizontalSnapOffsets):
+        (WebCore::ScrollingStateScrollingNode::verticalSnapOffsets):
+        (WebCore::ScrollingStateScrollingNode::horizontalSnapOffsetRanges):
+        (WebCore::ScrollingStateScrollingNode::verticalSnapOffsetRanges):
+        * page/scrolling/ScrollingTreeScrollingNode.cpp:
+        (WebCore::ScrollingTreeScrollingNode::commitStateBeforeChildren):
+        (WebCore::ScrollingTreeScrollingNode::dumpProperties):
+        * page/scrolling/ScrollingTreeScrollingNode.h:
+        (WebCore::ScrollingTreeScrollingNode::horizontalSnapOffsets):
+        (WebCore::ScrollingTreeScrollingNode::verticalSnapOffsets):
+        (WebCore::ScrollingTreeScrollingNode::horizontalSnapOffsetRanges):
+        (WebCore::ScrollingTreeScrollingNode::verticalSnapOffsetRanges):
+
+        Add more boilerplate support for snap offset ranges.
+
+        * page/scrolling/mac/ScrollingMomentumCalculatorMac.h:
+        * page/scrolling/mac/ScrollingMomentumCalculatorMac.mm:
+        (WebCore::ScrollingMomentumCalculator::create):
+        (WebCore::ScrollingMomentumCalculator::setPlatformMomentumScrollingPredictionEnabled):
+        (WebCore::ScrollingMomentumCalculatorMac::ScrollingMomentumCalculatorMac):
+        (WebCore::ScrollingMomentumCalculatorMac::scrollOffsetAfterElapsedTime):
+        (WebCore::ScrollingMomentumCalculatorMac::predictedDestinationOffset):
+        (WebCore::ScrollingMomentumCalculatorMac::retargetedScrollOffsetDidChange):
+        (WebCore::ScrollingMomentumCalculatorMac::animationDuration):
+        (WebCore::ScrollingMomentumCalculatorMac::requiresMomentumScrolling):
+        (WebCore::ScrollingMomentumCalculatorMac::ensurePlatformMomentumCalculator):
+
+        Hook into AppKit momentum scroll offset prediction.
+
+        * page/scrolling/mac/ScrollingTreeFrameScrollingNodeMac.mm:
+        (WebCore::convertToLayoutUnits):
+        (WebCore::ScrollingTreeFrameScrollingNodeMac::commitStateBeforeChildren):
+        * platform/ScrollableArea.cpp:
+        (WebCore::ScrollableArea::ensureSnapOffsetsInfo):
+        (WebCore::ScrollableArea::horizontalSnapOffsets):
+        (WebCore::ScrollableArea::horizontalSnapOffsetRanges):
+        (WebCore::ScrollableArea::verticalSnapOffsetRanges):
+        (WebCore::ScrollableArea::verticalSnapOffsets):
+        (WebCore::ScrollableArea::setHorizontalSnapOffsets):
+        (WebCore::ScrollableArea::setVerticalSnapOffsets):
+        (WebCore::ScrollableArea::setHorizontalSnapOffsetRanges):
+        (WebCore::ScrollableArea::setVerticalSnapOffsetRanges):
+        (WebCore::ScrollableArea::clearHorizontalSnapOffsets):
+        (WebCore::ScrollableArea::clearVerticalSnapOffsets):
+        * platform/ScrollableArea.h:
+        (WebCore::ScrollableArea::horizontalSnapOffsets): Deleted.
+        (WebCore::ScrollableArea::verticalSnapOffsets): Deleted.
+        * platform/cocoa/ScrollController.h:
+        * platform/cocoa/ScrollController.mm:
+        (WebCore::ScrollController::processWheelEventForScrollSnap):
+
+        Fix an issue where initial scrolling velocity would be set to zero at the end of a drag gesture.
+
+        (WebCore::ScrollController::updateScrollSnapState):
+        (WebCore::ScrollController::updateScrollSnapPoints):
+        (WebCore::ScrollController::setNearestScrollSnapIndexForAxisAndOffset):
+        * platform/cocoa/ScrollSnapAnimatorState.h:
+        (WebCore::ScrollSnapAnimatorState::snapOffsetsForAxis):
+        (WebCore::ScrollSnapAnimatorState::snapOffsetRangesForAxis):
+        (WebCore::ScrollSnapAnimatorState::setSnapOffsetsAndPositionRangesForAxis):
+        (WebCore::ScrollSnapAnimatorState::setSnapOffsetsForAxis): Deleted.
+        * platform/cocoa/ScrollSnapAnimatorState.mm:
+        (WebCore::ScrollSnapAnimatorState::setupAnimationForState):
+        (WebCore::ScrollSnapAnimatorState::targetOffsetForStartOffset):
+        (WebCore::projectedInertialScrollDistance): Deleted.
+        * rendering/RenderLayerCompositor.cpp:
+        (WebCore::RenderLayerCompositor::updateScrollCoordinatedLayer):
+        * testing/Internals.cpp:
+        (WebCore::Internals::setPlatformMomentumScrollingPredictionEnabled):
+
+        Add a new hook for layout tests to force scrolling momentum calculators to use the platform-invariant momentum
+        scrolling prediction heuristic instead of the platform-dependent one.
+
+        (WebCore::Internals::scrollSnapOffsets):
+        * testing/Internals.h:
+        * testing/Internals.idl:
+
 2017-01-10  Chris Dumez  <cdumez@apple.com>
 
         Make Event.initEvent()'s first parameter mandatory
index 33c95a2..bb93ab3 100644 (file)
                F44EBBDB1DB5DD9D00277334 /* StaticRange.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F44EBBDA1DB5DD9D00277334 /* StaticRange.cpp */; };
                F45C231D1995B73B00A6E2E3 /* AxisScrollSnapOffsets.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F45C231B1995B73B00A6E2E3 /* AxisScrollSnapOffsets.cpp */; };
                F45C231E1995B73B00A6E2E3 /* AxisScrollSnapOffsets.h in Headers */ = {isa = PBXBuildFile; fileRef = F45C231C1995B73B00A6E2E3 /* AxisScrollSnapOffsets.h */; settings = {ATTRIBUTES = (Private, ); }; };
+               F46729281E0DE68500ACC3D8 /* ScrollSnapOffsetsInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = F46729251E0DE5AB00ACC3D8 /* ScrollSnapOffsetsInfo.h */; settings = {ATTRIBUTES = (Private, ); }; };
                F478755419983AFF0024A287 /* ScrollSnapAnimatorState.h in Headers */ = {isa = PBXBuildFile; fileRef = F478755219983AFF0024A287 /* ScrollSnapAnimatorState.h */; settings = {ATTRIBUTES = (Private, ); }; };
                F478755519983AFF0024A287 /* ScrollSnapAnimatorState.mm in Sources */ = {isa = PBXBuildFile; fileRef = F478755319983AFF0024A287 /* ScrollSnapAnimatorState.mm */; };
                F47A5E3E195B8C8A00483100 /* StyleScrollSnapPoints.h in Headers */ = {isa = PBXBuildFile; fileRef = F47A5E3B195B8C8A00483100 /* StyleScrollSnapPoints.h */; settings = {ATTRIBUTES = (Private, ); }; };
                F44EBBDA1DB5DD9D00277334 /* StaticRange.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = StaticRange.cpp; sourceTree = "<group>"; };
                F45C231B1995B73B00A6E2E3 /* AxisScrollSnapOffsets.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = AxisScrollSnapOffsets.cpp; sourceTree = "<group>"; };
                F45C231C1995B73B00A6E2E3 /* AxisScrollSnapOffsets.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AxisScrollSnapOffsets.h; sourceTree = "<group>"; };
+               F46729251E0DE5AB00ACC3D8 /* ScrollSnapOffsetsInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ScrollSnapOffsetsInfo.h; sourceTree = "<group>"; };
                F478755219983AFF0024A287 /* ScrollSnapAnimatorState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ScrollSnapAnimatorState.h; sourceTree = "<group>"; };
                F478755319983AFF0024A287 /* ScrollSnapAnimatorState.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ScrollSnapAnimatorState.mm; sourceTree = "<group>"; };
                F47A5E3A195B8C8A00483100 /* StyleScrollSnapPoints.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = StyleScrollSnapPoints.cpp; sourceTree = "<group>"; };
                                0FEA3E7E191B3169000F1B55 /* ScrollingTreeOverflowScrollingNode.h */,
                                9391A99A1629D6FF00297330 /* ScrollingTreeScrollingNode.cpp */,
                                9391A99B1629D70000297330 /* ScrollingTreeScrollingNode.h */,
+                               F46729251E0DE5AB00ACC3D8 /* ScrollSnapOffsetsInfo.h */,
                                7AAFE8CD19CB8672000F56D8 /* ScrollLatchingState.cpp */,
                                7AAFE8CE19CB8672000F56D8 /* ScrollLatchingState.h */,
                                0F6383DB18615B29003E5DB5 /* ThreadedScrollingTree.cpp */,
                                0F4710C01DB56BE8002DCEC3 /* JSDOMRectReadOnly.h in Headers */,
                                BC5A86B60C3367E800EEA649 /* JSDOMSelection.h in Headers */,
                                C5137CF311A58378004ADB99 /* JSDOMStringList.h in Headers */,
+                               F46729281E0DE68500ACC3D8 /* ScrollSnapOffsetsInfo.h in Headers */,
                                BC64649811D82349006455B0 /* JSDOMStringMap.h in Headers */,
                                7694563D1214D97C0007CBAE /* JSDOMTokenList.h in Headers */,
                                2E37E00612DBC5A400A6B233 /* JSDOMURL.h in Headers */,
index f6e546b..74cf83d 100644 (file)
@@ -65,7 +65,7 @@ void AsyncScrollingCoordinator::scrollingStateTreePropertiesChanged()
     scheduleTreeStateCommit();
 }
 
-static inline void setStateScrollingNodeSnapOffsetsAsFloat(ScrollingStateScrollingNode& node, ScrollEventAxis axis, const Vector<LayoutUnit>* snapOffsets, float deviceScaleFactor)
+static inline void setStateScrollingNodeSnapOffsetsAsFloat(ScrollingStateScrollingNode& node, ScrollEventAxis axis, const Vector<LayoutUnit>* snapOffsets, const Vector<ScrollOffsetRange<LayoutUnit>>* snapOffsetRanges, float deviceScaleFactor)
 {
     // FIXME: Incorporate current page scale factor in snapping to device pixel. Perhaps we should just convert to float here and let UI process do the pixel snapping?
     Vector<float> snapOffsetsAsFloat;
@@ -74,10 +74,20 @@ static inline void setStateScrollingNodeSnapOffsetsAsFloat(ScrollingStateScrolli
         for (auto& offset : *snapOffsets)
             snapOffsetsAsFloat.uncheckedAppend(roundToDevicePixel(offset, deviceScaleFactor, false));
     }
-    if (axis == ScrollEventAxis::Horizontal)
+
+    Vector<ScrollOffsetRange<float>> snapOffsetRangesAsFloat;
+    if (snapOffsetRanges) {
+        snapOffsetRangesAsFloat.reserveInitialCapacity(snapOffsetRanges->size());
+        for (auto& range : *snapOffsetRanges)
+            snapOffsetRangesAsFloat.uncheckedAppend({ roundToDevicePixel(range.start, deviceScaleFactor, false), roundToDevicePixel(range.end, deviceScaleFactor, false) });
+    }
+    if (axis == ScrollEventAxis::Horizontal) {
         node.setHorizontalSnapOffsets(snapOffsetsAsFloat);
-    else
+        node.setHorizontalSnapOffsetRanges(snapOffsetRangesAsFloat);
+    } else {
         node.setVerticalSnapOffsets(snapOffsetsAsFloat);
+        node.setVerticalSnapOffsetRanges(snapOffsetRangesAsFloat);
+    }
 }
 
 void AsyncScrollingCoordinator::setEventTrackingRegionsDirty()
@@ -537,8 +547,8 @@ void AsyncScrollingCoordinator::updateOverflowScrollingNode(ScrollingNodeID node
         node->setReachableContentsSize(scrollingGeometry->reachableContentSize);
         node->setScrollableAreaSize(scrollingGeometry->scrollableAreaSize);
 #if ENABLE(CSS_SCROLL_SNAP)
-        setStateScrollingNodeSnapOffsetsAsFloat(*node, ScrollEventAxis::Horizontal, &scrollingGeometry->horizontalSnapOffsets, m_page->deviceScaleFactor());
-        setStateScrollingNodeSnapOffsetsAsFloat(*node, ScrollEventAxis::Vertical, &scrollingGeometry->verticalSnapOffsets, m_page->deviceScaleFactor());
+        setStateScrollingNodeSnapOffsetsAsFloat(*node, ScrollEventAxis::Horizontal, &scrollingGeometry->horizontalSnapOffsets, &scrollingGeometry->horizontalSnapOffsetRanges, m_page->deviceScaleFactor());
+        setStateScrollingNodeSnapOffsetsAsFloat(*node, ScrollEventAxis::Vertical, &scrollingGeometry->verticalSnapOffsets, &scrollingGeometry->verticalSnapOffsetRanges, m_page->deviceScaleFactor());
         node->setCurrentHorizontalSnapPointIndex(scrollingGeometry->currentHorizontalSnapPointIndex);
         node->setCurrentVerticalSnapPointIndex(scrollingGeometry->currentVerticalSnapPointIndex);
 #endif
@@ -682,8 +692,8 @@ bool AsyncScrollingCoordinator::isScrollSnapInProgress() const
 void AsyncScrollingCoordinator::updateScrollSnapPropertiesWithFrameView(const FrameView& frameView)
 {
     if (auto node = downcast<ScrollingStateFrameScrollingNode>(m_scrollingStateTree->stateNodeForID(frameView.scrollLayerID()))) {
-        setStateScrollingNodeSnapOffsetsAsFloat(*node, ScrollEventAxis::Horizontal, frameView.horizontalSnapOffsets(), m_page->deviceScaleFactor());
-        setStateScrollingNodeSnapOffsetsAsFloat(*node, ScrollEventAxis::Vertical, frameView.verticalSnapOffsets(), m_page->deviceScaleFactor());
+        setStateScrollingNodeSnapOffsetsAsFloat(*node, ScrollEventAxis::Horizontal, frameView.horizontalSnapOffsets(), frameView.horizontalSnapOffsetRanges(), m_page->deviceScaleFactor());
+        setStateScrollingNodeSnapOffsetsAsFloat(*node, ScrollEventAxis::Vertical, frameView.verticalSnapOffsets(), frameView.verticalSnapOffsetRanges(), m_page->deviceScaleFactor());
         node->setCurrentHorizontalSnapPointIndex(frameView.currentHorizontalSnapPointIndex());
         node->setCurrentVerticalSnapPointIndex(frameView.currentVerticalSnapPointIndex());
     }
index 469c08f..0207538 100644 (file)
@@ -84,6 +84,16 @@ static String snapOffsetsToString(const Vector<LayoutUnit>& snapOffsets)
     return s.toString();
 }
 
+static String snapOffsetRangesToString(const Vector<ScrollOffsetRange<LayoutUnit>>& ranges)
+{
+    StringBuilder s;
+    s.append("[ ");
+    for (auto range : ranges)
+        s.append(String::format("(%.1f, %.1f) ", range.start.toFloat(), range.end.toFloat()));
+    s.append("]");
+    return s.toString();
+}
+
 static String snapPortOrAreaToString(const LayoutRect& rect)
 {
     return String::format("{{%.1f, %.1f} {%.1f, %.1f}}", rect.x().toFloat(), rect.y().toFloat(), rect.width().toFloat(), rect.height().toFloat());
@@ -91,6 +101,100 @@ static String snapPortOrAreaToString(const LayoutRect& rect)
 
 #endif
 
+template <typename LayoutType>
+static void indicesOfNearestSnapOffsetRanges(LayoutType offset, const Vector<ScrollOffsetRange<LayoutType>>& snapOffsetRanges, unsigned& lowerIndex, unsigned& upperIndex)
+{
+    if (snapOffsetRanges.isEmpty()) {
+        lowerIndex = invalidSnapOffsetIndex;
+        upperIndex = invalidSnapOffsetIndex;
+        return;
+    }
+
+    int lowerIndexAsInt = -1;
+    int upperIndexAsInt = snapOffsetRanges.size();
+    do {
+        int middleIndex = (lowerIndexAsInt + upperIndexAsInt) / 2;
+        auto& range = snapOffsetRanges[middleIndex];
+        if (range.start < offset && offset < range.end) {
+            lowerIndexAsInt = middleIndex;
+            upperIndexAsInt = middleIndex;
+            break;
+        }
+
+        if (offset > range.end)
+            lowerIndexAsInt = middleIndex;
+        else
+            upperIndexAsInt = middleIndex;
+    } while (lowerIndexAsInt < upperIndexAsInt - 1);
+
+    if (offset <= snapOffsetRanges.first().start)
+        lowerIndex = invalidSnapOffsetIndex;
+    else
+        lowerIndex = lowerIndexAsInt;
+
+    if (offset >= snapOffsetRanges.last().end)
+        upperIndex = invalidSnapOffsetIndex;
+    else
+        upperIndex = upperIndexAsInt;
+}
+
+template <typename LayoutType>
+static void indicesOfNearestSnapOffsets(LayoutType offset, const Vector<LayoutType>& snapOffsets, unsigned& lowerIndex, unsigned& upperIndex)
+{
+    lowerIndex = 0;
+    upperIndex = snapOffsets.size() - 1;
+    while (lowerIndex < upperIndex - 1) {
+        int middleIndex = (lowerIndex + upperIndex) / 2;
+        auto middleOffset = snapOffsets[middleIndex];
+        if (offset == middleOffset) {
+            upperIndex = middleIndex;
+            lowerIndex = middleIndex;
+            break;
+        }
+
+        if (offset > middleOffset)
+            lowerIndex = middleIndex;
+        else
+            upperIndex = middleIndex;
+    }
+}
+
+static void adjustAxisSnapOffsetsForScrollExtent(Vector<LayoutUnit>& snapOffsets, float maxScrollExtent)
+{
+    if (snapOffsets.isEmpty())
+        return;
+
+    std::sort(snapOffsets.begin(), snapOffsets.end());
+    if (snapOffsets.last() != maxScrollExtent)
+        snapOffsets.append(maxScrollExtent);
+    if (snapOffsets.first())
+        snapOffsets.insert(0, 0);
+}
+
+static void computeAxisProximitySnapOffsetRanges(const Vector<LayoutUnit>& snapOffsets, Vector<ScrollOffsetRange<LayoutUnit>>& offsetRanges, LayoutUnit scrollPortAxisLength)
+{
+    // This is an arbitrary choice for what it means to be "in proximity" of a snap offset. We should play around with
+    // this and see what feels best.
+    static const float ratioOfScrollPortAxisLengthToBeConsideredForProximity = 0.3;
+    if (snapOffsets.size() < 2)
+        return;
+
+    // The extra rule accounting for scroll offset ranges in between the scroll destination and a potential snap offset
+    // handles the corner case where the user scrolls with momentum very lightly away from a snap offset, such that the
+    // predicted scroll destination is still within proximity of the snap offset. In this case, the regular (mandatory
+    // scroll snapping) behavior would be to snap to the next offset in the direction of momentum scrolling, but
+    // instead, it is more intuitive to either return to the original snap position (which we arbitrarily choose here)
+    // or scroll just outside of the snap offset range. This is another minor behavior tweak that we should play around
+    // with to see what feels best.
+    LayoutUnit proximityDistance = ratioOfScrollPortAxisLengthToBeConsideredForProximity * scrollPortAxisLength;
+    for (size_t index = 1; index < snapOffsets.size(); ++index) {
+        auto startOffset = snapOffsets[index - 1] + proximityDistance;
+        auto endOffset = snapOffsets[index] - proximityDistance;
+        if (startOffset < endOffset)
+            offsetRanges.append({ startOffset, endOffset });
+    }
+}
+
 void updateSnapOffsetsForScrollableArea(ScrollableArea& scrollableArea, HTMLElement& scrollingElement, const RenderBox& scrollingElementBox, const RenderStyle& scrollingElementStyle)
 {
     auto* scrollContainer = scrollingElement.renderer();
@@ -101,8 +205,10 @@ void updateSnapOffsetsForScrollableArea(ScrollableArea& scrollableArea, HTMLElem
         return;
     }
 
-    auto verticalSnapOffsets = std::make_unique<Vector<LayoutUnit>>();
-    auto horizontalSnapOffsets = std::make_unique<Vector<LayoutUnit>>();
+    Vector<LayoutUnit> verticalSnapOffsets;
+    Vector<LayoutUnit> horizontalSnapOffsets;
+    Vector<ScrollOffsetRange<LayoutUnit>> verticalSnapOffsetRanges;
+    Vector<ScrollOffsetRange<LayoutUnit>> horizontalSnapOffsetRanges;
     HashSet<float> seenVerticalSnapOffsets;
     HashSet<float> seenHorizontalSnapOffsets;
     bool hasHorizontalSnapOffsets = scrollSnapType.axis == ScrollSnapAxis::Both || scrollSnapType.axis == ScrollSnapAxis::XAxis || scrollSnapType.axis == ScrollSnapAxis::Inline;
@@ -133,45 +239,108 @@ void updateSnapOffsetsForScrollableArea(ScrollableArea& scrollableArea, HTMLElem
             auto absoluteScrollOffset = clampTo<LayoutUnit>(computeScrollSnapAlignOffset(scrollSnapArea.x(), scrollSnapArea.width(), alignment.x) - computeScrollSnapAlignOffset(scrollSnapPort.x(), scrollSnapPort.width(), alignment.x), 0, maxScrollLeft);
             if (!seenHorizontalSnapOffsets.contains(absoluteScrollOffset)) {
                 seenHorizontalSnapOffsets.add(absoluteScrollOffset);
-                horizontalSnapOffsets->append(absoluteScrollOffset);
+                horizontalSnapOffsets.append(absoluteScrollOffset);
             }
         }
         if (hasVerticalSnapOffsets && alignment.y != ScrollSnapAxisAlignType::None) {
             auto absoluteScrollOffset = clampTo<LayoutUnit>(computeScrollSnapAlignOffset(scrollSnapArea.y(), scrollSnapArea.height(), alignment.y) - computeScrollSnapAlignOffset(scrollSnapPort.y(), scrollSnapPort.height(), alignment.y), 0, maxScrollTop);
             if (!seenVerticalSnapOffsets.contains(absoluteScrollOffset)) {
                 seenVerticalSnapOffsets.add(absoluteScrollOffset);
-                verticalSnapOffsets->append(absoluteScrollOffset);
+                verticalSnapOffsets.append(absoluteScrollOffset);
             }
         }
     }
 
-    std::sort(horizontalSnapOffsets->begin(), horizontalSnapOffsets->end());
-    if (horizontalSnapOffsets->size()) {
-        if (horizontalSnapOffsets->last() != maxScrollLeft)
-            horizontalSnapOffsets->append(maxScrollLeft);
-        if (horizontalSnapOffsets->first())
-            horizontalSnapOffsets->insert(0, 0);
+    if (!horizontalSnapOffsets.isEmpty()) {
+        adjustAxisSnapOffsetsForScrollExtent(horizontalSnapOffsets, maxScrollLeft);
 #if !LOG_DISABLED
-        LOG(Scrolling, " => Computed horizontal scroll snap offsets: %s", snapOffsetsToString(*horizontalSnapOffsets).utf8().data());
+        LOG(Scrolling, " => Computed horizontal scroll snap offsets: %s", snapOffsetsToString(horizontalSnapOffsets).utf8().data());
+        LOG(Scrolling, " => Computed horizontal scroll snap offset ranges: %s", snapOffsetRangesToString(horizontalSnapOffsetRanges).utf8().data());
 #endif
-        scrollableArea.setHorizontalSnapOffsets(WTFMove(horizontalSnapOffsets));
+        if (scrollSnapType.strictness == ScrollSnapStrictness::Proximity)
+            computeAxisProximitySnapOffsetRanges(horizontalSnapOffsets, horizontalSnapOffsetRanges, scrollSnapPort.width());
+
+        scrollableArea.setHorizontalSnapOffsets(horizontalSnapOffsets);
+        scrollableArea.setHorizontalSnapOffsetRanges(horizontalSnapOffsetRanges);
     } else
         scrollableArea.clearHorizontalSnapOffsets();
 
-    std::sort(verticalSnapOffsets->begin(), verticalSnapOffsets->end());
-    if (verticalSnapOffsets->size()) {
-        if (verticalSnapOffsets->last() != maxScrollTop)
-            verticalSnapOffsets->append(maxScrollTop);
-        if (verticalSnapOffsets->first())
-            verticalSnapOffsets->insert(0, 0);
+    if (!verticalSnapOffsets.isEmpty()) {
+        adjustAxisSnapOffsetsForScrollExtent(verticalSnapOffsets, maxScrollTop);
 #if !LOG_DISABLED
-        LOG(Scrolling, " => Computed vertical scroll snap offsets: %s", snapOffsetsToString(*verticalSnapOffsets).utf8().data());
+        LOG(Scrolling, " => Computed vertical scroll snap offsets: %s", snapOffsetsToString(verticalSnapOffsets).utf8().data());
+        LOG(Scrolling, " => Computed vertical scroll snap offset ranges: %s", snapOffsetRangesToString(verticalSnapOffsetRanges).utf8().data());
 #endif
-        scrollableArea.setVerticalSnapOffsets(WTFMove(verticalSnapOffsets));
+        if (scrollSnapType.strictness == ScrollSnapStrictness::Proximity)
+            computeAxisProximitySnapOffsetRanges(verticalSnapOffsets, verticalSnapOffsetRanges, scrollSnapPort.height());
+
+        scrollableArea.setVerticalSnapOffsets(verticalSnapOffsets);
+        scrollableArea.setVerticalSnapOffsetRanges(verticalSnapOffsetRanges);
     } else
         scrollableArea.clearVerticalSnapOffsets();
 }
 
+template <typename LayoutType>
+LayoutType closestSnapOffset(const Vector<LayoutType>& snapOffsets, const Vector<ScrollOffsetRange<LayoutType>>& snapOffsetRanges, LayoutType scrollDestination, float velocity, unsigned& activeSnapIndex)
+{
+    ASSERT(snapOffsets.size());
+    activeSnapIndex = 0;
+
+    unsigned lowerSnapOffsetRangeIndex;
+    unsigned upperSnapOffsetRangeIndex;
+    indicesOfNearestSnapOffsetRanges<LayoutType>(scrollDestination, snapOffsetRanges, lowerSnapOffsetRangeIndex, upperSnapOffsetRangeIndex);
+    if (lowerSnapOffsetRangeIndex == upperSnapOffsetRangeIndex && upperSnapOffsetRangeIndex != invalidSnapOffsetIndex) {
+        activeSnapIndex = invalidSnapOffsetIndex;
+        return scrollDestination;
+    }
+
+    if (scrollDestination <= snapOffsets.first())
+        return snapOffsets.first();
+
+    activeSnapIndex = snapOffsets.size() - 1;
+    if (scrollDestination >= snapOffsets.last())
+        return snapOffsets.last();
+
+    unsigned lowerIndex;
+    unsigned upperIndex;
+    indicesOfNearestSnapOffsets<LayoutType>(scrollDestination, snapOffsets, lowerIndex, upperIndex);
+    LayoutType lowerSnapPosition = snapOffsets[lowerIndex];
+    LayoutType upperSnapPosition = snapOffsets[upperIndex];
+    if (!std::abs(velocity)) {
+        bool isCloserToLowerSnapPosition = scrollDestination - lowerSnapPosition <= upperSnapPosition - scrollDestination;
+        activeSnapIndex = isCloserToLowerSnapPosition ? lowerIndex : upperIndex;
+        return isCloserToLowerSnapPosition ? lowerSnapPosition : upperSnapPosition;
+    }
+
+    // Non-zero velocity indicates a flick gesture. Even if another snap point is closer, we should choose the one in the direction of the flick gesture
+    // as long as a scroll snap offset range does not lie between the scroll destination and the targeted snap offset.
+    if (velocity < 0) {
+        if (lowerSnapOffsetRangeIndex != invalidSnapOffsetIndex && lowerSnapPosition < snapOffsetRanges[lowerSnapOffsetRangeIndex].end) {
+            activeSnapIndex = upperIndex;
+            return upperSnapPosition;
+        }
+        activeSnapIndex = lowerIndex;
+        return lowerSnapPosition;
+    }
+
+    if (upperSnapOffsetRangeIndex != invalidSnapOffsetIndex && snapOffsetRanges[upperSnapOffsetRangeIndex].start < upperSnapPosition) {
+        activeSnapIndex = lowerIndex;
+        return lowerSnapPosition;
+    }
+    activeSnapIndex = upperIndex;
+    return upperSnapPosition;
+}
+
+LayoutUnit closestSnapOffset(const Vector<LayoutUnit>& snapOffsets, const Vector<ScrollOffsetRange<LayoutUnit>>& snapOffsetRanges, LayoutUnit scrollDestination, float velocity, unsigned& activeSnapIndex)
+{
+    return closestSnapOffset<LayoutUnit>(snapOffsets, snapOffsetRanges, scrollDestination, velocity, activeSnapIndex);
+}
+
+float closestSnapOffset(const Vector<float>& snapOffsets, const Vector<ScrollOffsetRange<float>>& snapOffsetRanges, float scrollDestination, float velocity, unsigned& activeSnapIndex)
+{
+    return closestSnapOffset<float>(snapOffsets, snapOffsetRanges, scrollDestination, velocity, activeSnapIndex);
+}
+
 } // namespace WebCore
 
 #endif // CSS_SCROLL_SNAP
index b01cb7d..6b6b2bc 100644 (file)
@@ -27,6 +27,8 @@
 
 #if ENABLE(CSS_SCROLL_SNAP)
 
+#include "LayoutUnit.h"
+#include "ScrollSnapOffsetsInfo.h"
 #include "ScrollTypes.h"
 #include <wtf/Vector.h>
 
@@ -39,47 +41,9 @@ class ScrollableArea;
 
 void updateSnapOffsetsForScrollableArea(ScrollableArea&, HTMLElement& scrollingElement, const RenderBox& scrollingElementBox, const RenderStyle& scrollingElementStyle);
 
-// closestSnapOffset is a templated function that takes in a Vector representing snap offsets as LayoutTypes (e.g. LayoutUnit or float) and
-// as well as a VelocityType indicating the velocity (e.g. float, CGFloat, etc.) This function is templated because the UI process will now
-// use pixel snapped floats to represent snap offsets rather than LayoutUnits.
-template <typename LayoutType, typename VelocityType>
-LayoutType closestSnapOffset(const Vector<LayoutType>& snapOffsets, LayoutType scrollDestination, VelocityType velocity, unsigned& activeSnapIndex)
-{
-    ASSERT(snapOffsets.size());
-    activeSnapIndex = 0;
-    if (scrollDestination <= snapOffsets.first())
-        return snapOffsets.first();
-
-    activeSnapIndex = snapOffsets.size() - 1;
-    if (scrollDestination >= snapOffsets.last())
-        return snapOffsets.last();
-
-    size_t lowerIndex = 0;
-    size_t upperIndex = snapOffsets.size() - 1;
-    while (lowerIndex < upperIndex - 1) {
-        size_t middleIndex = (lowerIndex + upperIndex) / 2;
-        if (scrollDestination < snapOffsets[middleIndex])
-            upperIndex = middleIndex;
-        else if (scrollDestination > snapOffsets[middleIndex])
-            lowerIndex = middleIndex;
-        else {
-            upperIndex = middleIndex;
-            lowerIndex = middleIndex;
-            break;
-        }
-    }
-    LayoutType lowerSnapPosition = snapOffsets[lowerIndex];
-    LayoutType upperSnapPosition = snapOffsets[upperIndex];
-    // Nonzero velocity indicates a flick gesture. Even if another snap point is closer, snap to the one in the direction of the flick gesture.
-    if (velocity) {
-        activeSnapIndex = (velocity < 0) ? lowerIndex : upperIndex;
-        return velocity < 0 ? lowerSnapPosition : upperSnapPosition;
-    }
-
-    bool isCloserToLowerSnapPosition = scrollDestination - lowerSnapPosition <= upperSnapPosition - scrollDestination;
-    activeSnapIndex = isCloserToLowerSnapPosition ? lowerIndex : upperIndex;
-    return isCloserToLowerSnapPosition ? lowerSnapPosition : upperSnapPosition;
-}
+const unsigned invalidSnapOffsetIndex = UINT_MAX;
+WEBCORE_EXPORT LayoutUnit closestSnapOffset(const Vector<LayoutUnit>& snapOffsets, const Vector<ScrollOffsetRange<LayoutUnit>>& snapOffsetRanges, LayoutUnit scrollDestination, float velocity, unsigned& activeSnapIndex);
+WEBCORE_EXPORT float closestSnapOffset(const Vector<float>& snapOffsets, const Vector<ScrollOffsetRange<float>>& snapOffsetRanges, float scrollDestination, float velocity, unsigned& activeSnapIndex);
 
 } // namespace WebCore
 
diff --git a/Source/WebCore/page/scrolling/ScrollSnapOffsetsInfo.h b/Source/WebCore/page/scrolling/ScrollSnapOffsetsInfo.h
new file mode 100644 (file)
index 0000000..63bd963
--- /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 <wtf/Vector.h>
+
+namespace WebCore {
+
+template <typename T>
+struct ScrollOffsetRange {
+    T start;
+    T end;
+};
+
+template <typename T>
+struct ScrollSnapOffsetsInfo {
+    Vector<T> horizontalSnapOffsets;
+    Vector<T> verticalSnapOffsets;
+
+    // Snap offset ranges represent non-empty ranges of scroll offsets in which scrolling may rest after scroll snapping.
+    // These are used in two cases: (1) for proximity scroll snapping, where portions of areas between adjacent snap offsets
+    // may emit snap offset ranges, and (2) in the case where the snap area is larger than the snap port, in which case areas
+    // where the snap port fits within the snap area are considered to be valid snap positions.
+    Vector<ScrollOffsetRange<T>> horizontalSnapOffsetRanges;
+    Vector<ScrollOffsetRange<T>> verticalSnapOffsetRanges;
+};
+
+}; // namespace WebCore
index 7a6af21..eb6dabf 100644 (file)
@@ -29,6 +29,7 @@
 #include "IntRect.h"
 #include "LayoutRect.h"
 #include "PlatformWheelEvent.h"
+#include "ScrollSnapOffsetsInfo.h"
 #include "ScrollTypes.h"
 #include <wtf/Forward.h>
 #include <wtf/ThreadSafeRefCounted.h>
@@ -170,6 +171,8 @@ public:
 #if ENABLE(CSS_SCROLL_SNAP)
         Vector<LayoutUnit> horizontalSnapOffsets;
         Vector<LayoutUnit> verticalSnapOffsets;
+        Vector<ScrollOffsetRange<LayoutUnit>> horizontalSnapOffsetRanges;
+        Vector<ScrollOffsetRange<LayoutUnit>> verticalSnapOffsetRanges;
         unsigned currentHorizontalSnapPointIndex;
         unsigned currentVerticalSnapPointIndex;
 #endif
index 8c69af2..1fa5674 100644 (file)
 namespace WebCore {
 
 static const Seconds scrollSnapAnimationDuration = 1_s;
+static inline float projectedInertialScrollDistance(float initialWheelDelta)
+{
+    // On macOS 10.10 and earlier, we don't have a platform scrolling momentum calculator, so we instead approximate the scroll destination
+    // by multiplying the initial wheel delta by a constant factor. By running a few experiments (i.e. logging scroll destination and initial
+    // wheel delta for many scroll gestures) we determined that this is a reasonable way to approximate where scrolling will take us without
+    // using _NSScrollingMomentumCalculator.
+    const static double inertialScrollPredictionFactor = 16.7;
+    return inertialScrollPredictionFactor * initialWheelDelta;
+}
 
-ScrollingMomentumCalculator::ScrollingMomentumCalculator(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatPoint& targetOffset, const FloatSize& initialDelta, const FloatSize& initialVelocity)
+ScrollingMomentumCalculator::ScrollingMomentumCalculator(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatSize& initialDelta, const FloatSize& 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)
 {
 }
 
+void ScrollingMomentumCalculator::setRetargetedScrollOffset(const FloatSize& target)
+{
+    if (m_retargetedScrollOffset && m_retargetedScrollOffset == target)
+        return;
+
+    m_retargetedScrollOffset = target;
+    retargetedScrollOffsetDidChange();
+}
+
+FloatSize ScrollingMomentumCalculator::predictedDestinationOffset()
+{
+    float initialOffsetX = clampTo<float>(m_initialScrollOffset.width() + projectedInertialScrollDistance(m_initialDelta.width()), 0, m_contentSize.width() - m_viewportSize.width());
+    float initialOffsetY = clampTo<float>(m_initialScrollOffset.height() + projectedInertialScrollDistance(m_initialDelta.height()), 0, m_contentSize.height() - m_viewportSize.height());
+    return { initialOffsetX, initialOffsetY };
+}
+
 #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 FloatSize& initialVelocity)
+std::unique_ptr<ScrollingMomentumCalculator> ScrollingMomentumCalculator::create(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatSize& initialDelta, const FloatSize& initialVelocity)
+{
+    return std::make_unique<BasicScrollingMomentumCalculator>(viewportSize, contentSize, initialOffset, initialDelta, initialVelocity);
+}
+
+void ScrollingMomentumCalculator::setPlatformMomentumScrollingPredictionEnabled(bool)
 {
-    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 FloatSize& initialVelocity)
-    : ScrollingMomentumCalculator(viewportSize, contentSize, initialOffset, targetOffset, initialDelta, initialVelocity)
+BasicScrollingMomentumCalculator::BasicScrollingMomentumCalculator(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatSize& initialDelta, const FloatSize& initialVelocity)
+    : ScrollingMomentumCalculator(viewportSize, contentSize, initialOffset, initialDelta, initialVelocity)
 {
 }
 
-FloatSize BasicScrollingMomentumCalculator::linearlyInterpolatedOffsetAtProgress(float progress) const
+FloatSize BasicScrollingMomentumCalculator::linearlyInterpolatedOffsetAtProgress(float progress)
 {
-    return m_initialScrollOffset + progress * (m_targetScrollOffset - m_initialScrollOffset);
+    return m_initialScrollOffset + progress * (retargetedScrollOffset() - m_initialScrollOffset);
 }
 
 FloatSize BasicScrollingMomentumCalculator::cubicallyInterpolatedOffsetAtProgress(float progress) const
@@ -120,7 +148,7 @@ void BasicScrollingMomentumCalculator::initializeInterpolationCoefficientsIfNece
         return;
     }
 
-    FloatSize startToEndVector = m_targetScrollOffset - m_initialScrollOffset;
+    FloatSize startToEndVector = retargetedScrollOffset() - 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.
@@ -140,7 +168,7 @@ void BasicScrollingMomentumCalculator::initializeInterpolationCoefficientsIfNece
     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_snapAnimationCurveCoefficients[3] = 3 * (controlVector1 - controlVector2) - m_initialScrollOffset + retargetedScrollOffset();
     m_forceLinearAnimationCurve = false;
 }
 
@@ -179,10 +207,10 @@ void BasicScrollingMomentumCalculator::initializeSnapProgressCurve()
     static const float minScrollSnapInitialProgress = 0.1;
     static const float maxScrollSnapInitialProgress = 0.5;
 
-    FloatSize alignmentVector = m_initialDelta * (m_targetScrollOffset - m_initialScrollOffset);
+    FloatSize alignmentVector = m_initialDelta * (retargetedScrollOffset() - m_initialScrollOffset);
     float initialProgress;
     if (alignmentVector.width() + alignmentVector.height() > 0)
-        initialProgress = clampTo(m_initialDelta.diagonalLength() / (m_targetScrollOffset - m_initialScrollOffset).diagonalLength(), minScrollSnapInitialProgress, maxScrollSnapInitialProgress);
+        initialProgress = clampTo(m_initialDelta.diagonalLength() / (retargetedScrollOffset() - m_initialScrollOffset).diagonalLength(), minScrollSnapInitialProgress, maxScrollSnapInitialProgress);
     else
         initialProgress = minScrollSnapInitialProgress;
 
index ced0f2a..a82afaa 100644 (file)
@@ -30,6 +30,7 @@
 #include "AxisScrollSnapOffsets.h"
 #include "PlatformWheelEvent.h"
 #include "ScrollTypes.h"
+#include <wtf/Optional.h>
 #include <wtf/Seconds.h>
 
 namespace WebCore {
@@ -39,25 +40,33 @@ class FloatSize;
 
 class ScrollingMomentumCalculator {
 public:
-    ScrollingMomentumCalculator(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatPoint& targetOffset, const FloatSize& initialDelta, const FloatSize& initialVelocity);
-    static std::unique_ptr<ScrollingMomentumCalculator> create(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatPoint& targetOffset, const FloatSize& initialDelta, const FloatSize& initialVelocity);
+    ScrollingMomentumCalculator(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatSize& initialDelta, const FloatSize& initialVelocity);
+    static std::unique_ptr<ScrollingMomentumCalculator> create(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatSize& initialDelta, const FloatSize& initialVelocity);
+    WEBCORE_EXPORT static void setPlatformMomentumScrollingPredictionEnabled(bool);
     virtual ~ScrollingMomentumCalculator() { }
 
     virtual FloatPoint scrollOffsetAfterElapsedTime(Seconds) = 0;
     virtual Seconds animationDuration() = 0;
+    virtual FloatSize predictedDestinationOffset();
+    void setRetargetedScrollOffset(const FloatSize&);
 
 protected:
+    const FloatSize& retargetedScrollOffset() const { return m_retargetedScrollOffset.value(); }
+    virtual void retargetedScrollOffsetDidChange() { }
+
     FloatSize m_initialDelta;
     FloatSize m_initialVelocity;
     FloatSize m_initialScrollOffset;
-    FloatSize m_targetScrollOffset;
     FloatSize m_viewportSize;
     FloatSize m_contentSize;
+
+private:
+    std::optional<FloatSize> m_retargetedScrollOffset;
 };
 
 class BasicScrollingMomentumCalculator final : public ScrollingMomentumCalculator {
 public:
-    BasicScrollingMomentumCalculator(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatPoint& targetOffset, const FloatSize& initialDelta, const FloatSize& initialVelocity);
+    BasicScrollingMomentumCalculator(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatSize& initialDelta, const FloatSize& initialVelocity);
 
 private:
     FloatPoint scrollOffsetAfterElapsedTime(Seconds) final;
@@ -65,7 +74,7 @@ private:
     void initializeInterpolationCoefficientsIfNecessary();
     void initializeSnapProgressCurve();
     float animationProgressAfterElapsedTime(Seconds) const;
-    FloatSize linearlyInterpolatedOffsetAtProgress(float progress) const;
+    FloatSize linearlyInterpolatedOffsetAtProgress(float progress);
     FloatSize cubicallyInterpolatedOffsetAtProgress(float progress) const;
 
     float m_snapAnimationCurveMagnitude { 0 };
@@ -73,6 +82,7 @@ private:
     FloatSize m_snapAnimationCurveCoefficients[4] { };
     bool m_forceLinearAnimationCurve { false };
     bool m_momentumCalculatorRequiresInitialization { true };
+    std::optional<FloatSize> m_predictedDestinationOffset;
 };
 
 } // namespace WebCore
index a5cfa12..b3de263 100644 (file)
@@ -47,8 +47,7 @@ ScrollingStateScrollingNode::ScrollingStateScrollingNode(const ScrollingStateScr
     , m_requestedScrollPosition(stateNode.requestedScrollPosition())
     , m_scrollOrigin(stateNode.scrollOrigin())
 #if ENABLE(CSS_SCROLL_SNAP)
-    , m_horizontalSnapOffsets(stateNode.horizontalSnapOffsets())
-    , m_verticalSnapOffsets(stateNode.verticalSnapOffsets())
+    , m_snapOffsetsInfo(stateNode.m_snapOffsetsInfo)
 #endif
     , m_scrollableAreaParameters(stateNode.scrollableAreaParameters())
     , m_requestedScrollPositionRepresentsProgrammaticScroll(stateNode.requestedScrollPositionRepresentsProgrammaticScroll())
@@ -108,22 +107,40 @@ void ScrollingStateScrollingNode::setScrollOrigin(const IntPoint& scrollOrigin)
 #if ENABLE(CSS_SCROLL_SNAP)
 void ScrollingStateScrollingNode::setHorizontalSnapOffsets(const Vector<float>& snapOffsets)
 {
-    if (m_horizontalSnapOffsets == snapOffsets)
+    if (m_snapOffsetsInfo.horizontalSnapOffsets == snapOffsets)
         return;
 
-    m_horizontalSnapOffsets = snapOffsets;
+    m_snapOffsetsInfo.horizontalSnapOffsets = snapOffsets;
     setPropertyChanged(HorizontalSnapOffsets);
 }
 
 void ScrollingStateScrollingNode::setVerticalSnapOffsets(const Vector<float>& snapOffsets)
 {
-    if (m_verticalSnapOffsets == snapOffsets)
+    if (m_snapOffsetsInfo.verticalSnapOffsets == snapOffsets)
         return;
 
-    m_verticalSnapOffsets = snapOffsets;
+    m_snapOffsetsInfo.verticalSnapOffsets = snapOffsets;
     setPropertyChanged(VerticalSnapOffsets);
 }
 
+void ScrollingStateScrollingNode::setHorizontalSnapOffsetRanges(const Vector<ScrollOffsetRange<float>>& scrollOffsetRanges)
+{
+    if (m_snapOffsetsInfo.horizontalSnapOffsetRanges == scrollOffsetRanges)
+        return;
+
+    m_snapOffsetsInfo.horizontalSnapOffsetRanges = scrollOffsetRanges;
+    setPropertyChanged(HorizontalSnapOffsetRanges);
+}
+
+void ScrollingStateScrollingNode::setVerticalSnapOffsetRanges(const Vector<ScrollOffsetRange<float>>& scrollOffsetRanges)
+{
+    if (m_snapOffsetsInfo.verticalSnapOffsetRanges == scrollOffsetRanges)
+        return;
+
+    m_snapOffsetsInfo.verticalSnapOffsetRanges = scrollOffsetRanges;
+    setPropertyChanged(VerticalSnapOffsetRanges);
+}
+
 void ScrollingStateScrollingNode::setCurrentHorizontalSnapPointIndex(unsigned index)
 {
     if (m_currentHorizontalSnapPointIndex == index)
index e823116..628c34f 100644 (file)
@@ -27,6 +27,7 @@
 
 #if ENABLE(ASYNC_SCROLLING) || USE(COORDINATED_GRAPHICS)
 
+#include "ScrollSnapOffsetsInfo.h"
 #include "ScrollTypes.h"
 #include "ScrollingCoordinator.h"
 #include "ScrollingStateNode.h"
@@ -49,6 +50,8 @@ public:
 #if ENABLE(CSS_SCROLL_SNAP)
         HorizontalSnapOffsets,
         VerticalSnapOffsets,
+        HorizontalSnapOffsetRanges,
+        VerticalSnapOffsetRanges,
         CurrentHorizontalSnapOffsetIndex,
         CurrentVerticalSnapOffsetIndex,
 #endif
@@ -71,12 +74,18 @@ public:
     WEBCORE_EXPORT void setScrollOrigin(const IntPoint&);
 
 #if ENABLE(CSS_SCROLL_SNAP)
-    const Vector<float>& horizontalSnapOffsets() const { return m_horizontalSnapOffsets; }
+    const Vector<float>& horizontalSnapOffsets() const { return m_snapOffsetsInfo.horizontalSnapOffsets; }
     WEBCORE_EXPORT void setHorizontalSnapOffsets(const Vector<float>&);
 
-    const Vector<float>& verticalSnapOffsets() const { return m_verticalSnapOffsets; }
+    const Vector<float>& verticalSnapOffsets() const { return m_snapOffsetsInfo.verticalSnapOffsets; }
     WEBCORE_EXPORT void setVerticalSnapOffsets(const Vector<float>&);
 
+    const Vector<ScrollOffsetRange<float>>& horizontalSnapOffsetRanges() const { return m_snapOffsetsInfo.horizontalSnapOffsetRanges; }
+    WEBCORE_EXPORT void setHorizontalSnapOffsetRanges(const Vector<ScrollOffsetRange<float>>&);
+
+    const Vector<ScrollOffsetRange<float>>& verticalSnapOffsetRanges() const { return m_snapOffsetsInfo.verticalSnapOffsetRanges; }
+    WEBCORE_EXPORT void setVerticalSnapOffsetRanges(const Vector<ScrollOffsetRange<float>>&);
+
     unsigned currentHorizontalSnapPointIndex() const { return m_currentHorizontalSnapPointIndex; }
     WEBCORE_EXPORT void setCurrentHorizontalSnapPointIndex(unsigned);
 
@@ -108,8 +117,7 @@ private:
     FloatPoint m_requestedScrollPosition;
     IntPoint m_scrollOrigin;
 #if ENABLE(CSS_SCROLL_SNAP)
-    Vector<float> m_horizontalSnapOffsets;
-    Vector<float> m_verticalSnapOffsets;
+    ScrollSnapOffsetsInfo<float> m_snapOffsetsInfo;
     unsigned m_currentHorizontalSnapPointIndex { 0 };
     unsigned m_currentVerticalSnapPointIndex { 0 };
 #endif
index 84029fb..19754f5 100644 (file)
@@ -70,10 +70,16 @@ void ScrollingTreeScrollingNode::commitStateBeforeChildren(const ScrollingStateN
 
 #if ENABLE(CSS_SCROLL_SNAP)
     if (state.hasChangedProperty(ScrollingStateScrollingNode::HorizontalSnapOffsets))
-        m_horizontalSnapOffsets = state.horizontalSnapOffsets();
+        m_snapOffsetsInfo.horizontalSnapOffsets = state.horizontalSnapOffsets();
 
     if (state.hasChangedProperty(ScrollingStateScrollingNode::VerticalSnapOffsets))
-        m_verticalSnapOffsets = state.verticalSnapOffsets();
+        m_snapOffsetsInfo.verticalSnapOffsets = state.verticalSnapOffsets();
+
+    if (state.hasChangedProperty(ScrollingStateScrollingNode::HorizontalSnapOffsetRanges))
+        m_snapOffsetsInfo.horizontalSnapOffsetRanges = state.horizontalSnapOffsetRanges();
+
+    if (state.hasChangedProperty(ScrollingStateScrollingNode::VerticalSnapOffsetRanges))
+        m_snapOffsetsInfo.verticalSnapOffsetRanges = state.verticalSnapOffsetRanges();
 
     if (state.hasChangedProperty(ScrollingStateScrollingNode::CurrentHorizontalSnapOffsetIndex))
         m_currentHorizontalSnapPointIndex = state.currentHorizontalSnapPointIndex();
@@ -139,14 +145,14 @@ void ScrollingTreeScrollingNode::dumpProperties(TextStream& ts, ScrollingStateTr
         ts.dumpProperty("scrollable area size", m_scrollOrigin);
 
 #if ENABLE(CSS_SCROLL_SNAP)
-    if (m_horizontalSnapOffsets.size())
-        ts.dumpProperty("horizontal snap offsets", m_horizontalSnapOffsets);
+    if (m_snapOffsetsInfo.horizontalSnapOffsets.size())
+        ts.dumpProperty("horizontal snap offsets", m_snapOffsetsInfo.horizontalSnapOffsets);
 
-    if (m_verticalSnapOffsets.size())
-        ts.dumpProperty("horizontal snap offsets", m_verticalSnapOffsets);
+    if (m_snapOffsetsInfo.verticalSnapOffsets.size())
+        ts.dumpProperty("horizontal snap offsets", m_snapOffsetsInfo.verticalSnapOffsets);
 
     if (m_currentHorizontalSnapPointIndex)
-        ts.dumpProperty("current horizontal snap point index", m_verticalSnapOffsets);
+        ts.dumpProperty("current horizontal snap point index", m_currentHorizontalSnapPointIndex);
 
     if (m_currentVerticalSnapPointIndex)
         ts.dumpProperty("current vertical snap point index", m_currentVerticalSnapPointIndex);
index fa830b2..3594b10 100644 (file)
@@ -28,6 +28,7 @@
 #if ENABLE(ASYNC_SCROLLING)
 
 #include "IntRect.h"
+#include "ScrollSnapOffsetsInfo.h"
 #include "ScrollTypes.h"
 #include "ScrollingCoordinator.h"
 #include "ScrollingTreeNode.h"
@@ -56,8 +57,10 @@ public:
     virtual FloatPoint scrollPosition() const = 0;
 
 #if ENABLE(CSS_SCROLL_SNAP)
-    const Vector<float>& horizontalSnapOffsets() const { return m_horizontalSnapOffsets; }
-    const Vector<float>& verticalSnapOffsets() const { return m_verticalSnapOffsets; }
+    const Vector<float>& horizontalSnapOffsets() const { return m_snapOffsetsInfo.horizontalSnapOffsets; }
+    const Vector<float>& verticalSnapOffsets() const { return m_snapOffsetsInfo.verticalSnapOffsets; }
+    const Vector<ScrollOffsetRange<float>>& horizontalSnapOffsetRanges() const { return m_snapOffsetsInfo.horizontalSnapOffsetRanges; }
+    const Vector<ScrollOffsetRange<float>>& verticalSnapOffsetRanges() const { return m_snapOffsetsInfo.verticalSnapOffsetRanges; }
     unsigned currentHorizontalSnapPointIndex() const { return m_currentHorizontalSnapPointIndex; }
     unsigned currentVerticalSnapPointIndex() const { return m_currentVerticalSnapPointIndex; }
     void setCurrentHorizontalSnapPointIndex(unsigned index) { m_currentHorizontalSnapPointIndex = index; }
@@ -102,8 +105,7 @@ private:
     FloatPoint m_lastCommittedScrollPosition;
     IntPoint m_scrollOrigin;
 #if ENABLE(CSS_SCROLL_SNAP)
-    Vector<float> m_horizontalSnapOffsets;
-    Vector<float> m_verticalSnapOffsets;
+    ScrollSnapOffsetsInfo<float> m_snapOffsetsInfo;
     unsigned m_currentHorizontalSnapPointIndex { 0 };
     unsigned m_currentVerticalSnapPointIndex { 0 };
 #endif
index 456a113..c90503c 100644 (file)
@@ -36,15 +36,19 @@ 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 FloatSize& initialVelocity);
+    ScrollingMomentumCalculatorMac(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatSize& initialDelta, const FloatSize& initialVelocity);
 
 private:
     FloatPoint scrollOffsetAfterElapsedTime(Seconds) final;
     Seconds animationDuration() final;
+    FloatSize predictedDestinationOffset() final;
+    void retargetedScrollOffsetDidChange() final;
     _NSScrollingMomentumCalculator *ensurePlatformMomentumCalculator();
+    bool requiresMomentumScrolling();
 
     RetainPtr<_NSScrollingMomentumCalculator> m_platformMomentumCalculator;
-    bool m_requiresMomentumScrolling { true };
+    std::optional<bool> m_requiresMomentumScrolling;
+    FloatPoint m_initialDestinationOrigin;
 };
 
 } // namespace WebCore
index f72455e..1f851dd 100644 (file)
 
 namespace WebCore {
 
-std::unique_ptr<ScrollingMomentumCalculator> ScrollingMomentumCalculator::create(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatPoint& targetOffset, const FloatSize& initialDelta, const FloatSize& initialVelocity)
+static bool gEnablePlatformMomentumScrollingPrediction = true;
+
+std::unique_ptr<ScrollingMomentumCalculator> ScrollingMomentumCalculator::create(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatSize& initialDelta, const FloatSize& initialVelocity)
 {
-    return std::make_unique<ScrollingMomentumCalculatorMac>(viewportSize, contentSize, initialOffset, targetOffset, initialDelta, initialVelocity);
+    return std::make_unique<ScrollingMomentumCalculatorMac>(viewportSize, contentSize, initialOffset, initialDelta, initialVelocity);
 }
 
-ScrollingMomentumCalculatorMac::ScrollingMomentumCalculatorMac(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatPoint& targetOffset, const FloatSize& initialDelta, const FloatSize& initialVelocity)
-    : ScrollingMomentumCalculator(viewportSize, contentSize, initialOffset, targetOffset, initialDelta, initialVelocity)
-    , m_requiresMomentumScrolling(initialOffset != targetOffset || initialVelocity.area())
+void ScrollingMomentumCalculator::setPlatformMomentumScrollingPredictionEnabled(bool enabled)
+{
+    gEnablePlatformMomentumScrollingPrediction = enabled;
+}
+
+ScrollingMomentumCalculatorMac::ScrollingMomentumCalculatorMac(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatSize& initialDelta, const FloatSize& initialVelocity)
+    : ScrollingMomentumCalculator(viewportSize, contentSize, initialOffset, initialDelta, initialVelocity)
 {
 }
 
 FloatPoint ScrollingMomentumCalculatorMac::scrollOffsetAfterElapsedTime(Seconds elapsedTime)
 {
-    if (!m_requiresMomentumScrolling)
-        return { m_targetScrollOffset.width(), m_targetScrollOffset.height() };
+    if (!requiresMomentumScrolling())
+        return { retargetedScrollOffset().width(), retargetedScrollOffset().height() };
 
     return [ensurePlatformMomentumCalculator() positionAfterDuration:elapsedTime.value()];
 }
 
+FloatSize ScrollingMomentumCalculatorMac::predictedDestinationOffset()
+{
+    if (!gEnablePlatformMomentumScrollingPrediction)
+        return ScrollingMomentumCalculator::predictedDestinationOffset();
+
+    ensurePlatformMomentumCalculator();
+    return { m_initialDestinationOrigin.x(), m_initialDestinationOrigin.y() };
+}
+
+void ScrollingMomentumCalculatorMac::retargetedScrollOffsetDidChange()
+{
+    _NSScrollingMomentumCalculator *calculator = ensurePlatformMomentumCalculator();
+    calculator.destinationOrigin = NSMakePoint(retargetedScrollOffset().width(), retargetedScrollOffset().height());
+#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 101200
+    [calculator calculateToReachDestination];
+#endif
+}
+
 Seconds ScrollingMomentumCalculatorMac::animationDuration()
 {
-    if (!m_requiresMomentumScrolling)
+    if (!requiresMomentumScrolling())
         return 0_s;
 
     return Seconds([ensurePlatformMomentumCalculator() durationUntilStop]);
 }
 
+bool ScrollingMomentumCalculatorMac::requiresMomentumScrolling()
+{
+    if (m_requiresMomentumScrolling == std::nullopt)
+        m_requiresMomentumScrolling = m_initialScrollOffset != retargetedScrollOffset() || m_initialVelocity.area();
+    return m_requiresMomentumScrolling.value();
+}
+
 _NSScrollingMomentumCalculator *ScrollingMomentumCalculatorMac::ensurePlatformMomentumCalculator()
 {
     if (m_platformMomentumCalculator)
@@ -68,10 +99,7 @@ _NSScrollingMomentumCalculator *ScrollingMomentumCalculatorMac::ensurePlatformMo
     NSRect contentFrame = NSMakeRect(0, 0, m_contentSize.width(), m_contentSize.height());
     NSPoint velocity = NSMakePoint(m_initialVelocity.width(), m_initialVelocity.height());
     m_platformMomentumCalculator = adoptNS([[_NSScrollingMomentumCalculator alloc] initWithInitialOrigin:origin velocity:velocity documentFrame:contentFrame constrainedClippingOrigin:NSZeroPoint clippingSize:m_viewportSize tolerance:NSMakeSize(1, 1)]);
-    [m_platformMomentumCalculator setDestinationOrigin:NSMakePoint(m_targetScrollOffset.width(), m_targetScrollOffset.height())];
-#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 101200
-    [m_platformMomentumCalculator calculateToReachDestination];
-#endif
+    m_initialDestinationOrigin = [m_platformMomentumCalculator destinationOrigin];
     return m_platformMomentumCalculator.get();
 }
 
index 11e7a74..9a07ffb 100644 (file)
@@ -90,6 +90,16 @@ static inline Vector<LayoutUnit> convertToLayoutUnits(const Vector<float>& snapO
 
     return snapOffsets;
 }
+
+static inline Vector<ScrollOffsetRange<LayoutUnit>> convertToLayoutUnits(const Vector<ScrollOffsetRange<float>>& snapOffsetRangesAsFloat)
+{
+    Vector<ScrollOffsetRange<LayoutUnit>> snapOffsetRanges;
+    snapOffsetRanges.reserveInitialCapacity(snapOffsetRangesAsFloat.size());
+    for (auto range : snapOffsetRangesAsFloat)
+        snapOffsetRanges.uncheckedAppend({ range.start, range.end });
+
+    return snapOffsetRanges;
+}
 #endif
 
 void ScrollingTreeFrameScrollingNodeMac::commitStateBeforeChildren(const ScrollingStateNode& stateNode)
@@ -146,11 +156,11 @@ void ScrollingTreeFrameScrollingNodeMac::commitStateBeforeChildren(const Scrolli
     }
 
 #if ENABLE(CSS_SCROLL_SNAP)
-    if (scrollingStateNode.hasChangedProperty(ScrollingStateFrameScrollingNode::HorizontalSnapOffsets))
-        m_scrollController.updateScrollSnapPoints(ScrollEventAxis::Horizontal, convertToLayoutUnits(scrollingStateNode.horizontalSnapOffsets()));
+    if (scrollingStateNode.hasChangedProperty(ScrollingStateFrameScrollingNode::HorizontalSnapOffsets) || scrollingStateNode.hasChangedProperty(ScrollingStateFrameScrollingNode::HorizontalSnapOffsetRanges))
+        m_scrollController.updateScrollSnapPoints(ScrollEventAxis::Horizontal, convertToLayoutUnits(scrollingStateNode.horizontalSnapOffsets()), convertToLayoutUnits(scrollingStateNode.horizontalSnapOffsetRanges()));
 
-    if (scrollingStateNode.hasChangedProperty(ScrollingStateFrameScrollingNode::VerticalSnapOffsets))
-        m_scrollController.updateScrollSnapPoints(ScrollEventAxis::Vertical, convertToLayoutUnits(scrollingStateNode.verticalSnapOffsets()));
+    if (scrollingStateNode.hasChangedProperty(ScrollingStateFrameScrollingNode::VerticalSnapOffsets) || scrollingStateNode.hasChangedProperty(ScrollingStateFrameScrollingNode::VerticalSnapOffsetRanges))
+        m_scrollController.updateScrollSnapPoints(ScrollEventAxis::Vertical, convertToLayoutUnits(scrollingStateNode.verticalSnapOffsets()), convertToLayoutUnits(scrollingStateNode.verticalSnapOffsetRanges()));
 
     if (scrollingStateNode.hasChangedProperty(ScrollingStateScrollingNode::CurrentHorizontalSnapOffsetIndex))
         m_scrollController.setActiveScrollSnapIndexForAxis(ScrollEventAxis::Horizontal, scrollingStateNode.currentHorizontalSnapPointIndex());
index a71b079..dead721 100644 (file)
@@ -48,7 +48,7 @@ namespace WebCore {
 struct SameSizeAsScrollableArea {
     virtual ~SameSizeAsScrollableArea();
 #if ENABLE(CSS_SCROLL_SNAP)
-    void* pointers[4];
+    void* pointers[3];
     unsigned currentIndices[2];
 #else
     void* pointer[2];
@@ -425,35 +425,90 @@ bool ScrollableArea::hasLayerForScrollCorner() const
 }
 
 #if ENABLE(CSS_SCROLL_SNAP)
-void ScrollableArea::setHorizontalSnapOffsets(std::unique_ptr<Vector<LayoutUnit>> horizontalSnapOffsets)
+ScrollSnapOffsetsInfo<LayoutUnit>& ScrollableArea::ensureSnapOffsetsInfo()
+{
+    if (!m_snapOffsetsInfo)
+        m_snapOffsetsInfo = std::make_unique<ScrollSnapOffsetsInfo<LayoutUnit>>();
+    return *m_snapOffsetsInfo;
+}
+
+const Vector<LayoutUnit>* ScrollableArea::horizontalSnapOffsets() const
+{
+    if (!m_snapOffsetsInfo)
+        return nullptr;
+
+    return &m_snapOffsetsInfo->horizontalSnapOffsets;
+}
+
+const Vector<ScrollOffsetRange<LayoutUnit>>* ScrollableArea::horizontalSnapOffsetRanges() const
+{
+    if (!m_snapOffsetsInfo)
+        return nullptr;
+
+    return &m_snapOffsetsInfo->horizontalSnapOffsetRanges;
+}
+
+const Vector<ScrollOffsetRange<LayoutUnit>>* ScrollableArea::verticalSnapOffsetRanges() const
+{
+    if (!m_snapOffsetsInfo)
+        return nullptr;
+
+    return &m_snapOffsetsInfo->verticalSnapOffsetRanges;
+}
+
+const Vector<LayoutUnit>* ScrollableArea::verticalSnapOffsets() const
+{
+    if (!m_snapOffsetsInfo)
+        return nullptr;
+
+    return &m_snapOffsetsInfo->verticalSnapOffsets;
+}
+
+void ScrollableArea::setHorizontalSnapOffsets(const Vector<LayoutUnit>& horizontalSnapOffsets)
 {
-    ASSERT(horizontalSnapOffsets);
     // Consider having a non-empty set of snap offsets as a cue to initialize the ScrollAnimator.
-    if (horizontalSnapOffsets->size())
+    if (horizontalSnapOffsets.size())
         scrollAnimator();
 
-    m_horizontalSnapOffsets = WTFMove(horizontalSnapOffsets);
+    ensureSnapOffsetsInfo().horizontalSnapOffsets = horizontalSnapOffsets;
 }
 
-void ScrollableArea::setVerticalSnapOffsets(std::unique_ptr<Vector<LayoutUnit>> verticalSnapOffsets)
+void ScrollableArea::setVerticalSnapOffsets(const Vector<LayoutUnit>& verticalSnapOffsets)
 {
-    ASSERT(verticalSnapOffsets);
     // Consider having a non-empty set of snap offsets as a cue to initialize the ScrollAnimator.
-    if (verticalSnapOffsets->size())
+    if (verticalSnapOffsets.size())
         scrollAnimator();
 
-    m_verticalSnapOffsets = WTFMove(verticalSnapOffsets);
+    ensureSnapOffsetsInfo().verticalSnapOffsets = verticalSnapOffsets;
+}
+
+void ScrollableArea::setHorizontalSnapOffsetRanges(const Vector<ScrollOffsetRange<LayoutUnit>>& horizontalRanges)
+{
+    ensureSnapOffsetsInfo().horizontalSnapOffsetRanges = horizontalRanges;
+}
+
+void ScrollableArea::setVerticalSnapOffsetRanges(const Vector<ScrollOffsetRange<LayoutUnit>>& verticalRanges)
+{
+    ensureSnapOffsetsInfo().verticalSnapOffsetRanges = verticalRanges;
 }
 
 void ScrollableArea::clearHorizontalSnapOffsets()
 {
-    m_horizontalSnapOffsets = nullptr;
+    if (!m_snapOffsetsInfo)
+        return;
+
+    m_snapOffsetsInfo->horizontalSnapOffsets = { };
+    m_snapOffsetsInfo->horizontalSnapOffsetRanges = { };
     m_currentHorizontalSnapPointIndex = 0;
 }
 
 void ScrollableArea::clearVerticalSnapOffsets()
 {
-    m_verticalSnapOffsets = nullptr;
+    if (!m_snapOffsetsInfo)
+        return;
+
+    m_snapOffsetsInfo->verticalSnapOffsets = { };
+    m_snapOffsetsInfo->verticalSnapOffsetRanges = { };
     m_currentVerticalSnapPointIndex = 0;
 }
 
index 8909715..24f8b16 100644 (file)
@@ -26,6 +26,7 @@
 #ifndef ScrollableArea_h
 #define ScrollableArea_h
 
+#include "ScrollSnapOffsetsInfo.h"
 #include "Scrollbar.h"
 #include <wtf/Vector.h>
 #include <wtf/WeakPtr.h>
@@ -67,11 +68,15 @@ public:
     WeakPtr<ScrollableArea> createWeakPtr() { return m_weakPtrFactory.createWeakPtr(); }
 
 #if ENABLE(CSS_SCROLL_SNAP)
-    const Vector<LayoutUnit>* horizontalSnapOffsets() const { return m_horizontalSnapOffsets.get(); };
-    const Vector<LayoutUnit>* verticalSnapOffsets() const { return m_verticalSnapOffsets.get(); };
+    WEBCORE_EXPORT const Vector<LayoutUnit>* horizontalSnapOffsets() const;
+    WEBCORE_EXPORT const Vector<LayoutUnit>* verticalSnapOffsets() const;
+    WEBCORE_EXPORT const Vector<ScrollOffsetRange<LayoutUnit>>* horizontalSnapOffsetRanges() const;
+    WEBCORE_EXPORT const Vector<ScrollOffsetRange<LayoutUnit>>* verticalSnapOffsetRanges() const;
     virtual void updateSnapOffsets() { };
-    void setHorizontalSnapOffsets(std::unique_ptr<Vector<LayoutUnit>>);
-    void setVerticalSnapOffsets(std::unique_ptr<Vector<LayoutUnit>>);
+    void setHorizontalSnapOffsets(const Vector<LayoutUnit>&);
+    void setVerticalSnapOffsets(const Vector<LayoutUnit>&);
+    void setHorizontalSnapOffsetRanges(const Vector<ScrollOffsetRange<LayoutUnit>>&);
+    void setVerticalSnapOffsetRanges(const Vector<ScrollOffsetRange<LayoutUnit>>&);
     void clearHorizontalSnapOffsets();
     void clearVerticalSnapOffsets();
     unsigned currentHorizontalSnapPointIndex() const { return m_currentHorizontalSnapPointIndex; }
@@ -346,14 +351,14 @@ private:
     // This function should be overriden by subclasses to perform the actual
     // scroll of the content.
     virtual void setScrollOffset(const ScrollOffset&) = 0;
+    ScrollSnapOffsetsInfo<LayoutUnit>& ensureSnapOffsetsInfo();
 
     mutable std::unique_ptr<ScrollAnimator> m_scrollAnimator;
 
     WeakPtrFactory<ScrollableArea> m_weakPtrFactory { this };
 
 #if ENABLE(CSS_SCROLL_SNAP)
-    std::unique_ptr<Vector<LayoutUnit>> m_horizontalSnapOffsets;
-    std::unique_ptr<Vector<LayoutUnit>> m_verticalSnapOffsets;
+    std::unique_ptr<ScrollSnapOffsetsInfo<LayoutUnit>> m_snapOffsetsInfo;
     unsigned m_currentHorizontalSnapPointIndex { 0 };
     unsigned m_currentVerticalSnapPointIndex { 0 };
 #endif
index b1c7a0c..0e44789 100644 (file)
@@ -37,6 +37,7 @@
 
 #if ENABLE(CSS_SCROLL_SNAP)
 #include "ScrollSnapAnimatorState.h"
+#include "ScrollSnapOffsetsInfo.h"
 #endif
 
 namespace WebCore {
@@ -133,7 +134,7 @@ public:
     bool isScrollSnapInProgress() const;
 
 #if ENABLE(CSS_SCROLL_SNAP)
-    void updateScrollSnapPoints(ScrollEventAxis, const Vector<LayoutUnit>&);
+    void updateScrollSnapPoints(ScrollEventAxis, const Vector<LayoutUnit>&, const Vector<ScrollOffsetRange<LayoutUnit>>&);
     void setActiveScrollSnapIndexForAxis(ScrollEventAxis, unsigned);
     void setActiveScrollSnapIndicesForOffset(int x, int y);
     bool activeScrollSnapIndexDidChange() const { return m_activeScrollSnapIndexDidChange; }
index e46ec56..a87d33b 100644 (file)
@@ -553,14 +553,15 @@ bool ScrollController::processWheelEventForScrollSnap(const PlatformWheelEvent&
     case WheelEventStatus::UserScrolling:
         stopScrollSnapTimer();
         m_scrollSnapState->transitionToUserInteractionState();
+        m_dragEndedScrollingVelocity = -wheelEvent.scrollingVelocity();
         break;
     case WheelEventStatus::UserScrollEnd:
-        m_dragEndedScrollingVelocity = -wheelEvent.scrollingVelocity();
         m_scrollSnapState->transitionToSnapAnimationState(m_client.scrollExtent(), m_client.viewportSize(), m_client.pageScaleFactor(), m_client.scrollOffset());
         startScrollSnapTimer();
         break;
     case WheelEventStatus::InertialScrollBegin:
         m_scrollSnapState->transitionToGlideAnimationState(m_client.scrollExtent(), m_client.viewportSize(), m_client.pageScaleFactor(), m_client.scrollOffset(), m_dragEndedScrollingVelocity, FloatSize(-wheelEvent.deltaX(), -wheelEvent.deltaY()));
+        m_dragEndedScrollingVelocity = { };
         isInertialScrolling = true;
         break;
     case WheelEventStatus::InertialScrolling:
@@ -581,18 +582,24 @@ bool ScrollController::processWheelEventForScrollSnap(const PlatformWheelEvent&
 
 void ScrollController::updateScrollSnapState(const ScrollableArea& scrollableArea)
 {
-    if (auto* snapOffsets = scrollableArea.horizontalSnapOffsets())
-        updateScrollSnapPoints(ScrollEventAxis::Horizontal, *snapOffsets);
-    else
-        updateScrollSnapPoints(ScrollEventAxis::Horizontal, { });
+    if (auto* snapOffsets = scrollableArea.horizontalSnapOffsets()) {
+        if (auto* snapOffsetRanges = scrollableArea.horizontalSnapOffsetRanges())
+            updateScrollSnapPoints(ScrollEventAxis::Horizontal, *snapOffsets, *snapOffsetRanges);
+        else
+            updateScrollSnapPoints(ScrollEventAxis::Horizontal, *snapOffsets, { });
+    } else
+        updateScrollSnapPoints(ScrollEventAxis::Horizontal, { }, { });
 
-    if (auto* snapOffsets = scrollableArea.verticalSnapOffsets())
-        updateScrollSnapPoints(ScrollEventAxis::Vertical, *snapOffsets);
-    else
-        updateScrollSnapPoints(ScrollEventAxis::Vertical, { });
+    if (auto* snapOffsets = scrollableArea.verticalSnapOffsets()) {
+        if (auto* snapOffsetRanges = scrollableArea.verticalSnapOffsetRanges())
+            updateScrollSnapPoints(ScrollEventAxis::Vertical, *snapOffsets, *snapOffsetRanges);
+        else
+            updateScrollSnapPoints(ScrollEventAxis::Vertical, *snapOffsets, { });
+    } else
+        updateScrollSnapPoints(ScrollEventAxis::Vertical, { }, { });
 }
 
-void ScrollController::updateScrollSnapPoints(ScrollEventAxis axis, const Vector<LayoutUnit>& snapPoints)
+void ScrollController::updateScrollSnapPoints(ScrollEventAxis axis, const Vector<LayoutUnit>& snapPoints, const Vector<ScrollOffsetRange<LayoutUnit>>& snapRanges)
 {
     if (!m_scrollSnapState) {
         if (snapPoints.isEmpty())
@@ -604,7 +611,7 @@ void ScrollController::updateScrollSnapPoints(ScrollEventAxis axis, const Vector
     if (snapPoints.isEmpty() && m_scrollSnapState->snapOffsetsForAxis(otherScrollEventAxis(axis)).isEmpty())
         m_scrollSnapState = nullptr;
     else
-        m_scrollSnapState->setSnapOffsetsForAxis(axis, snapPoints);
+        m_scrollSnapState->setSnapOffsetsAndPositionRangesForAxis(axis, snapPoints, snapRanges);
 }
 
 void ScrollController::startScrollSnapTimer()
@@ -680,7 +687,7 @@ void ScrollController::setNearestScrollSnapIndexForAxisAndOffset(ScrollEventAxis
     LayoutUnit clampedOffset = std::min(std::max(LayoutUnit(offset / scaleFactor), snapOffsets.first()), snapOffsets.last());
 
     unsigned activeIndex = 0;
-    closestSnapOffset<LayoutUnit, float>(snapState.snapOffsetsForAxis(axis), clampedOffset, 0, activeIndex);
+    closestSnapOffset(snapState.snapOffsetsForAxis(axis), snapState.snapOffsetRangesForAxis(axis), clampedOffset, 0, activeIndex);
 
     if (activeIndex == activeScrollSnapIndexForAxis(axis))
         return;
index 8f26e72..150db0b 100644 (file)
@@ -33,6 +33,7 @@
 #include "FloatSize.h"
 #include "LayoutPoint.h"
 #include "PlatformWheelEvent.h"
+#include "ScrollSnapOffsetsInfo.h"
 #include "ScrollTypes.h"
 #include "ScrollingMomentumCalculator.h"
 #include <wtf/MonotonicTime.h>
@@ -48,17 +49,25 @@ enum class ScrollSnapState {
 
 class ScrollSnapAnimatorState {
 public:
-    Vector<LayoutUnit> snapOffsetsForAxis(ScrollEventAxis axis) const
+    const Vector<LayoutUnit>& snapOffsetsForAxis(ScrollEventAxis axis) const
     {
         return axis == ScrollEventAxis::Horizontal ? m_snapOffsetsX : m_snapOffsetsY;
     }
 
-    void setSnapOffsetsForAxis(ScrollEventAxis axis, const Vector<LayoutUnit>& snapOffsets)
+    const Vector<ScrollOffsetRange<LayoutUnit>>& snapOffsetRangesForAxis(ScrollEventAxis axis) const
     {
-        if (axis == ScrollEventAxis::Horizontal)
+        return axis == ScrollEventAxis::Horizontal ? m_snapOffsetRangesX : m_snapOffsetRangesY;
+    }
+
+    void setSnapOffsetsAndPositionRangesForAxis(ScrollEventAxis axis, const Vector<LayoutUnit>& snapOffsets, const Vector<ScrollOffsetRange<LayoutUnit>>& snapOffsetRanges)
+    {
+        if (axis == ScrollEventAxis::Horizontal) {
             m_snapOffsetsX = snapOffsets;
-        else
+            m_snapOffsetRangesX = snapOffsetRanges;
+        } else {
             m_snapOffsetsY = snapOffsets;
+            m_snapOffsetRangesY = snapOffsetRanges;
+        }
     }
 
     ScrollSnapState currentState() const { return m_currentState; }
@@ -85,15 +94,17 @@ public:
     void transitionToDestinationReachedState();
 
 private:
-    float targetOffsetForStartOffset(ScrollEventAxis, float maxScrollOffset, float startOffset, float pageScale, float delta, unsigned& outActiveSnapIndex) const;
+    float targetOffsetForStartOffset(const Vector<LayoutUnit>& snapOffsets, const Vector<ScrollOffsetRange<LayoutUnit>>& snapOffsetRanges, float maxScrollOffset, float startOffset, float predictedOffset, 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 FloatSize& initialVelocity, const FloatSize& initialDelta);
 
     ScrollSnapState m_currentState { ScrollSnapState::UserInteraction };
 
     Vector<LayoutUnit> m_snapOffsetsX;
+    Vector<ScrollOffsetRange<LayoutUnit>> m_snapOffsetRangesX;
     unsigned m_activeSnapIndexX { 0 };
     Vector<LayoutUnit> m_snapOffsetsY;
+    Vector<ScrollOffsetRange<LayoutUnit>> m_snapOffsetRangesY;
     unsigned m_activeSnapIndexY { 0 };
 
     MonotonicTime m_startTime;
index 7e20181..e549409 100644 (file)
 
 namespace WebCore {
 
-static const float inertialScrollPredictionFactor = 10;
-static inline float projectedInertialScrollDistance(float initialWheelDelta)
-{
-    return inertialScrollPredictionFactor * initialWheelDelta;
-}
-
 void ScrollSnapAnimatorState::transitionToSnapAnimationState(const FloatSize& contentSize, const FloatSize& viewportSize, float pageScale, const FloatPoint& initialOffset)
 {
     setupAnimationForState(ScrollSnapState::Snapping, contentSize, viewportSize, pageScale, initialOffset, { }, { });
@@ -53,9 +47,11 @@ void ScrollSnapAnimatorState::setupAnimationForState(ScrollSnapState state, cons
     if (m_currentState == state)
         return;
 
-    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_momentumCalculator = ScrollingMomentumCalculator::create(viewportSize, contentSize, initialOffset, initialDelta, initialVelocity);
+    auto predictedScrollTarget = m_momentumCalculator->predictedDestinationOffset();
+    float targetOffsetX = targetOffsetForStartOffset(m_snapOffsetsX, m_snapOffsetRangesX, contentSize.width() - viewportSize.width(), initialOffset.x(), predictedScrollTarget.width(), pageScale, initialDelta.width(), m_activeSnapIndexX);
+    float targetOffsetY = targetOffsetForStartOffset(m_snapOffsetsY, m_snapOffsetRangesY, contentSize.height() - viewportSize.height(), initialOffset.y(), predictedScrollTarget.height(), pageScale, initialDelta.height(), m_activeSnapIndexY);
+    m_momentumCalculator->setRetargetedScrollOffset({ targetOffsetX, targetOffsetY });
     m_startTime = MonotonicTime::now();
     m_currentState = state;
 }
@@ -93,18 +89,17 @@ FloatPoint ScrollSnapAnimatorState::currentAnimatedScrollOffset(bool& isAnimatio
     return m_momentumCalculator->scrollOffsetAfterElapsedTime(elapsedTime);
 }
 
-float ScrollSnapAnimatorState::targetOffsetForStartOffset(ScrollEventAxis axis, float maxScrollOffset, float startOffset, float pageScale, float initialDelta, unsigned& outActiveSnapIndex) const
+float ScrollSnapAnimatorState::targetOffsetForStartOffset(const Vector<LayoutUnit>& snapOffsets, const Vector<ScrollOffsetRange<LayoutUnit>>& snapOffsetRanges, float maxScrollOffset, float startOffset, float predictedOffset, float pageScale, float initialDelta, unsigned& outActiveSnapIndex) const
 {
-    auto snapOffsets = snapOffsetsForAxis(axis);
-    if (!snapOffsets.size()) {
-        outActiveSnapIndex = 0;
+    if (snapOffsets.isEmpty()) {
+        outActiveSnapIndex = invalidSnapOffsetIndex;
         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);
+    float targetOffset = closestSnapOffset(snapOffsets, snapOffsetRanges, predictedOffset / pageScale, initialDelta, outActiveSnapIndex);
+    float minimumTargetOffset = std::max<float>(0, snapOffsets.first());
+    float maximumTargetOffset = std::min<float>(maxScrollOffset, snapOffsets.last());
+    targetOffset = clampTo<float>(targetOffset, minimumTargetOffset, maximumTargetOffset);
     return pageScale * targetOffset;
 }
     
index 2186390..e7e18ca 100644 (file)
@@ -3907,6 +3907,10 @@ void RenderLayerCompositor::updateScrollCoordinatedLayer(RenderLayer& layer, Lay
                 scrollingGeometry.horizontalSnapOffsets = *offsets;
             if (const Vector<LayoutUnit>* offsets = layer.verticalSnapOffsets())
                 scrollingGeometry.verticalSnapOffsets = *offsets;
+            if (const Vector<ScrollOffsetRange<LayoutUnit>>* ranges = layer.horizontalSnapOffsetRanges())
+                scrollingGeometry.horizontalSnapOffsetRanges = *ranges;
+            if (const Vector<ScrollOffsetRange<LayoutUnit>>* ranges = layer.verticalSnapOffsetRanges())
+                scrollingGeometry.verticalSnapOffsetRanges = *ranges;
             scrollingGeometry.currentHorizontalSnapPointIndex = layer.currentHorizontalSnapPointIndex();
             scrollingGeometry.currentVerticalSnapPointIndex = layer.currentVerticalSnapPointIndex();
 #endif
index 03dcbf2..8e8022f 100644 (file)
 #include "SchemeRegistry.h"
 #include "ScriptedAnimationController.h"
 #include "ScrollingCoordinator.h"
+#include "ScrollingMomentumCalculator.h"
 #include "SerializedScriptValue.h"
 #include "Settings.h"
 #include "ShadowRoot.h"
@@ -3340,6 +3341,11 @@ static void appendOffsets(StringBuilder& builder, const Vector<LayoutUnit>& snap
     builder.appendLiteral(" }");
 }
     
+void Internals::setPlatformMomentumScrollingPredictionEnabled(bool enabled)
+{
+    ScrollingMomentumCalculator::setPlatformMomentumScrollingPredictionEnabled(enabled);
+}
+
 ExceptionOr<String> Internals::scrollSnapOffsets(Element& element)
 {
     if (!element.renderBox())
@@ -3367,17 +3373,21 @@ ExceptionOr<String> Internals::scrollSnapOffsets(Element& element)
     
     StringBuilder result;
 
-    if (scrollableArea->horizontalSnapOffsets()) {
-        result.appendLiteral("horizontal = ");
-        appendOffsets(result, *scrollableArea->horizontalSnapOffsets());
+    if (auto* offsets = scrollableArea->horizontalSnapOffsets()) {
+        if (offsets->size()) {
+            result.appendLiteral("horizontal = ");
+            appendOffsets(result, *offsets);
+        }
     }
 
-    if (scrollableArea->verticalSnapOffsets()) {
-        if (result.length())
-            result.appendLiteral(", ");
+    if (auto* offsets = scrollableArea->verticalSnapOffsets()) {
+        if (offsets->size()) {
+            if (result.length())
+                result.appendLiteral(", ");
 
-        result.appendLiteral("vertical = ");
-        appendOffsets(result, *scrollableArea->verticalSnapOffsets());
+            result.appendLiteral("vertical = ");
+            appendOffsets(result, *offsets);
+        }
     }
 
     return result.toString();
index b3afe05..8361f73 100644 (file)
@@ -479,6 +479,7 @@ public:
 
 #if ENABLE(CSS_SCROLL_SNAP)
     ExceptionOr<String> scrollSnapOffsets(Element&);
+    void setPlatformMomentumScrollingPredictionEnabled(bool);
 #endif
 
     ExceptionOr<String> pathStringWithShrinkWrappedRects(const Vector<double>& rectComponents, double radius);
index 841a87c..0d71b8d 100644 (file)
@@ -456,6 +456,7 @@ enum EventThrottlingBehavior {
 
 #if defined(ENABLE_CSS_SCROLL_SNAP) && ENABLE_CSS_SCROLL_SNAP
     [MayThrowException] DOMString scrollSnapOffsets(Element element);
+    void setPlatformMomentumScrollingPredictionEnabled(boolean enabled);
 #endif
 
     [MayThrowException] DOMString pathStringWithShrinkWrappedRects(sequence<double> rectComponents, double radius);
index f223786..20dfe32 100644 (file)
@@ -1,3 +1,33 @@
+2017-01-10  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        Implement "proximity" scroll snapping
+        https://bugs.webkit.org/show_bug.cgi?id=135994
+        <rdar://problem/18162418>
+
+        Reviewed by Dean Jackson.
+
+        Adds boilerplate support for plumbing lists of snap offset ranges from the web process to the UI process
+        alongside the list of snap offsets.
+
+        * Shared/Scrolling/RemoteScrollingCoordinatorTransaction.cpp:
+        (ArgumentCoder<ScrollingStateScrollingNode>::encode):
+        (ArgumentCoder<ScrollingStateScrollingNode>::decode):
+        * Shared/WebCoreArgumentCoders.cpp:
+        (IPC::ArgumentCoder<ScrollOffsetRange<float>>::encode):
+        (IPC::ArgumentCoder<ScrollOffsetRange<float>>::decode):
+        * Shared/WebCoreArgumentCoders.h:
+        * UIProcess/Scrolling/ios/ScrollingTreeOverflowScrollingNodeIOS.mm:
+        (-[WKOverflowScrollViewDelegate scrollViewWillEndDragging:withVelocity:targetContentOffset:]):
+        * UIProcess/ios/RemoteScrollingCoordinatorProxyIOS.mm:
+
+        Adjust mainframe proximity scroll snapping logic to not subtract out the top content inset when there is no
+        active snap offset (i.e. when snapping rests in a snap offset range). Attempting to subtract out the top inset
+        in this case caused the scroll offset to jump after ending a drag with no momentum in a snap offset range.
+
+        (WebKit::RemoteScrollingCoordinatorProxy::adjustTargetContentOffsetForSnapping):
+        (WebKit::RemoteScrollingCoordinatorProxy::shouldSnapForMainFrameScrolling):
+        (WebKit::RemoteScrollingCoordinatorProxy::closestSnapOffsetForMainFrameScrolling):
+
 2017-01-10  Zan Dobersek  <zdobersek@igalia.com>
 
         ThreadedCoordinatedLayerTreeHost::renderNextFrame() should short-cut to layer flushing
index bbde74c..a055701 100644 (file)
@@ -127,6 +127,8 @@ void ArgumentCoder<ScrollingStateScrollingNode>::encode(Encoder& encoder, const
 #if ENABLE(CSS_SCROLL_SNAP)
     SCROLLING_NODE_ENCODE(ScrollingStateScrollingNode::HorizontalSnapOffsets, horizontalSnapOffsets)
     SCROLLING_NODE_ENCODE(ScrollingStateScrollingNode::VerticalSnapOffsets, verticalSnapOffsets)
+    SCROLLING_NODE_ENCODE(ScrollingStateScrollingNode::HorizontalSnapOffsetRanges, horizontalSnapOffsetRanges)
+    SCROLLING_NODE_ENCODE(ScrollingStateScrollingNode::VerticalSnapOffsetRanges, verticalSnapOffsetRanges)
     SCROLLING_NODE_ENCODE(ScrollingStateScrollingNode::CurrentHorizontalSnapOffsetIndex, currentHorizontalSnapPointIndex)
     SCROLLING_NODE_ENCODE(ScrollingStateScrollingNode::CurrentVerticalSnapOffsetIndex, currentVerticalSnapPointIndex)
 #endif
@@ -202,6 +204,8 @@ bool ArgumentCoder<ScrollingStateScrollingNode>::decode(Decoder& decoder, Scroll
 #if ENABLE(CSS_SCROLL_SNAP)
     SCROLLING_NODE_DECODE(ScrollingStateScrollingNode::HorizontalSnapOffsets, Vector<float>, setHorizontalSnapOffsets);
     SCROLLING_NODE_DECODE(ScrollingStateScrollingNode::VerticalSnapOffsets, Vector<float>, setVerticalSnapOffsets);
+    SCROLLING_NODE_DECODE(ScrollingStateScrollingNode::HorizontalSnapOffsetRanges, Vector<ScrollOffsetRange<float>>, setHorizontalSnapOffsetRanges)
+    SCROLLING_NODE_DECODE(ScrollingStateScrollingNode::VerticalSnapOffsetRanges, Vector<ScrollOffsetRange<float>>, setVerticalSnapOffsetRanges)
     SCROLLING_NODE_DECODE(ScrollingStateScrollingNode::CurrentHorizontalSnapOffsetIndex, unsigned, setCurrentHorizontalSnapPointIndex);
     SCROLLING_NODE_DECODE(ScrollingStateScrollingNode::CurrentVerticalSnapOffsetIndex, unsigned, setCurrentVerticalSnapPointIndex);
 #endif
index 1470997..c6a8571 100644 (file)
@@ -2472,4 +2472,29 @@ bool ArgumentCoder<IDBKeyPath>::decode(Decoder& decoder, IDBKeyPath& keyPath)
 }
 #endif
 
+#if ENABLE(CSS_SCROLL_SNAP)
+
+void ArgumentCoder<ScrollOffsetRange<float>>::encode(Encoder& encoder, const ScrollOffsetRange<float>& range)
+{
+    encoder << range.start;
+    encoder << range.end;
+}
+
+bool ArgumentCoder<ScrollOffsetRange<float>>::decode(Decoder& decoder, ScrollOffsetRange<float>& range)
+{
+    float start;
+    if (!decoder.decode(start))
+        return false;
+
+    float end;
+    if (!decoder.decode(end))
+        return false;
+
+    range.start = start;
+    range.end = end;
+    return true;
+}
+
+#endif
+
 } // namespace IPC
index 4819fcb..4437cad 100644 (file)
@@ -30,6 +30,7 @@
 #include <WebCore/FrameLoaderTypes.h>
 #include <WebCore/IndexedDB.h>
 #include <WebCore/PaymentHeaders.h>
+#include <WebCore/ScrollSnapOffsetsInfo.h>
 
 namespace WTF {
 class MonotonicTime;
@@ -599,6 +600,15 @@ template<> struct ArgumentCoder<WebCore::IDBKeyPath> {
 
 #endif
 
+#if ENABLE(CSS_SCROLL_SNAP)
+
+template<> struct ArgumentCoder<WebCore::ScrollOffsetRange<float>> {
+    static void encode(Encoder&, const WebCore::ScrollOffsetRange<float>&);
+    static bool decode(Decoder&, WebCore::ScrollOffsetRange<float>&);
+};
+
+#endif
+
 } // namespace IPC
 
 namespace WTF {
index c551ce8..879a98d 100644 (file)
@@ -39,6 +39,7 @@
 
 #if ENABLE(CSS_SCROLL_SNAP)
 #import <WebCore/AxisScrollSnapOffsets.h>
+#import <WebCore/ScrollSnapOffsetsInfo.h>
 #endif
 
 using namespace WebCore;
@@ -88,7 +89,7 @@ using namespace WebCore;
 
     if (!_scrollingTreeNode->horizontalSnapOffsets().isEmpty()) {
         unsigned index;
-        float potentialSnapPosition = closestSnapOffset<float, CGFloat>(_scrollingTreeNode->horizontalSnapOffsets(), horizontalTarget, velocity.x, index);
+        float potentialSnapPosition = closestSnapOffset(_scrollingTreeNode->horizontalSnapOffsets(), _scrollingTreeNode->horizontalSnapOffsetRanges(), horizontalTarget, velocity.x, index);
         _scrollingTreeNode->setCurrentHorizontalSnapPointIndex(index);
         if (horizontalTarget >= 0 && horizontalTarget <= scrollView.contentSize.width)
             targetContentOffset->x = potentialSnapPosition;
@@ -96,7 +97,7 @@ using namespace WebCore;
 
     if (!_scrollingTreeNode->verticalSnapOffsets().isEmpty()) {
         unsigned index;
-        float potentialSnapPosition = closestSnapOffset<float, CGFloat>(_scrollingTreeNode->verticalSnapOffsets(), verticalTarget, velocity.y, index);
+        float potentialSnapPosition = closestSnapOffset(_scrollingTreeNode->verticalSnapOffsets(), _scrollingTreeNode->verticalSnapOffsetRanges(), verticalTarget, velocity.y, index);
         _scrollingTreeNode->setCurrentVerticalSnapPointIndex(index);
         if (verticalTarget >= 0 && verticalTarget <= scrollView.contentSize.height)
             targetContentOffset->y = potentialSnapPosition;
index 6c9a1c0..dcaa47e 100644 (file)
@@ -39,6 +39,7 @@
 
 #if ENABLE(CSS_SCROLL_SNAP)
 #import <WebCore/AxisScrollSnapOffsets.h>
+#import <WebCore/ScrollSnapOffsetsInfo.h>
 #import <WebCore/ScrollTypes.h>
 #import <WebCore/ScrollingTreeFrameScrollingNode.h>
 #endif
@@ -124,7 +125,9 @@ void RemoteScrollingCoordinatorProxy::adjustTargetContentOffsetForSnapping(CGSiz
 
     if (shouldSnapForMainFrameScrolling(WebCore::ScrollEventAxis::Vertical)) {
         float potentialSnapPosition = closestSnapOffsetForMainFrameScrolling(WebCore::ScrollEventAxis::Vertical, targetContentOffset->y, velocity.y, m_currentVerticalSnapPointIndex);
-        potentialSnapPosition -= topInset;
+        if (m_currentVerticalSnapPointIndex != invalidSnapOffsetIndex)
+            potentialSnapPosition -= topInset;
+
         if (targetContentOffset->y > 0 && targetContentOffset->y < maxScrollOffsets.height)
             targetContentOffset->y = std::min<float>(maxScrollOffsets.height, potentialSnapPosition);
     }
@@ -142,7 +145,7 @@ bool RemoteScrollingCoordinatorProxy::shouldSnapForMainFrameScrolling(ScrollEven
         ScrollingTreeFrameScrollingNode* rootFrame = static_cast<ScrollingTreeFrameScrollingNode*>(root);
         const Vector<float>& snapOffsets = axis == ScrollEventAxis::Horizontal ? rootFrame->horizontalSnapOffsets() : rootFrame->verticalSnapOffsets();
         unsigned currentIndex = axis == ScrollEventAxis::Horizontal ? m_currentHorizontalSnapPointIndex : m_currentVerticalSnapPointIndex;
-        return (snapOffsets.size() > 0) && (currentIndex < snapOffsets.size());
+        return snapOffsets.size() && (currentIndex < snapOffsets.size() || currentIndex == invalidSnapOffsetIndex);
     }
     return false;
 }
@@ -153,9 +156,10 @@ float RemoteScrollingCoordinatorProxy::closestSnapOffsetForMainFrameScrolling(Sc
     ASSERT(root && root->isFrameScrollingNode());
     ScrollingTreeFrameScrollingNode* rootFrame = static_cast<ScrollingTreeFrameScrollingNode*>(root);
     const Vector<float>& snapOffsets = axis == ScrollEventAxis::Horizontal ? rootFrame->horizontalSnapOffsets() : rootFrame->verticalSnapOffsets();
+    const Vector<ScrollOffsetRange<float>>& snapOffsetRanges = axis == ScrollEventAxis::Horizontal ? rootFrame->horizontalSnapOffsetRanges() : rootFrame->verticalSnapOffsetRanges();
 
     float scaledScrollDestination = scrollDestination / m_webPageProxy.displayedContentScale();
-    float rawClosestSnapOffset = closestSnapOffset<float, float>(snapOffsets, scaledScrollDestination, velocity, currentIndex);
+    float rawClosestSnapOffset = closestSnapOffset(snapOffsets, snapOffsetRanges, scaledScrollDestination, velocity, currentIndex);
     return rawClosestSnapOffset * m_webPageProxy.displayedContentScale();
 }