Title: [279272] trunk/Source/WebCore
Revision
279272
Author
[email protected]
Date
2021-06-25 02:03:50 -0700 (Fri, 25 Jun 2021)

Log Message

[css-scroll-snap] Simplify snap point selection helpers
https://bugs.webkit.org/show_bug.cgi?id=227062

Reviewed by Frédéric Wang.

Combine indicesOfNearestSnapOffsets and findFirstSnapStopOffsetBetweenOriginAndDestination
and have the helper return the geometric previous and next snap points. This makes the
code a bit easier to read, slightly more efficient, and prepares for looking at
the geometric previous and next snap points for certain upcoming CSS Scroll Snap features.

Previously, indicesOfNearestSnapOffsets used a binary search and findFirstSnapStopOffsetBetweenOriginAndDestination
used a linear search. This change replaces these two functions with a single linear search.
While findFirstSnapStopOffsetBetweenOriginAndDestination is only called for directional scrolling,
directional scrolling is more common than non-directional scrolling (which is typically only called
by scrollIntoView() and other JS scrolling APIs). I have tested this change with a large table of
snap points and performance does not seem to be changed. In addition, a future change should mean
that this searchForPotentialSnapPoints should be called much less.

No new tests. This should not change behavior in any way.

* page/scrolling/ScrollSnapOffsetsInfo.cpp:
(WebCore::searchForPotentialSnapPoints): Added this new helper which is the combination
of indicesOfNearestSnapOffsets and findFirstSnapStopOffsetBetweenOriginAndDestination.
(WebCore::closestSnapOffsetWithInfoAndAxis): Use the new helper.

Modified Paths

Diff

Modified: trunk/Source/WebCore/ChangeLog (279271 => 279272)


--- trunk/Source/WebCore/ChangeLog	2021-06-25 08:59:13 UTC (rev 279271)
+++ trunk/Source/WebCore/ChangeLog	2021-06-25 09:03:50 UTC (rev 279272)
@@ -1,3 +1,30 @@
+2021-06-25  Martin Robinson  <[email protected]>
+
+        [css-scroll-snap] Simplify snap point selection helpers
+        https://bugs.webkit.org/show_bug.cgi?id=227062
+
+        Reviewed by Frédéric Wang.
+
+        Combine indicesOfNearestSnapOffsets and findFirstSnapStopOffsetBetweenOriginAndDestination
+        and have the helper return the geometric previous and next snap points. This makes the
+        code a bit easier to read, slightly more efficient, and prepares for looking at
+        the geometric previous and next snap points for certain upcoming CSS Scroll Snap features.
+
+        Previously, indicesOfNearestSnapOffsets used a binary search and findFirstSnapStopOffsetBetweenOriginAndDestination
+        used a linear search. This change replaces these two functions with a single linear search.
+        While findFirstSnapStopOffsetBetweenOriginAndDestination is only called for directional scrolling,
+        directional scrolling is more common than non-directional scrolling (which is typically only called
+        by scrollIntoView() and other JS scrolling APIs). I have tested this change with a large table of
+        snap points and performance does not seem to be changed. In addition, a future change should mean
+        that this searchForPotentialSnapPoints should be called much less.
+
+        No new tests. This should not change behavior in any way.
+
+        * page/scrolling/ScrollSnapOffsetsInfo.cpp:
+        (WebCore::searchForPotentialSnapPoints): Added this new helper which is the combination
+        of indicesOfNearestSnapOffsets and findFirstSnapStopOffsetBetweenOriginAndDestination.
+        (WebCore::closestSnapOffsetWithInfoAndAxis): Use the new helper.
+
 2021-06-14  Sergio Villar Senin  <[email protected]>
 
         [css-flexbox] Do not clamp flex base size with {min|max}-{height|width}

Modified: trunk/Source/WebCore/page/scrolling/ScrollSnapOffsetsInfo.cpp (279271 => 279272)


--- trunk/Source/WebCore/page/scrolling/ScrollSnapOffsetsInfo.cpp	2021-06-25 08:59:13 UTC (rev 279271)
+++ trunk/Source/WebCore/page/scrolling/ScrollSnapOffsetsInfo.cpp	2021-06-25 09:03:50 UTC (rev 279272)
@@ -40,58 +40,46 @@
 
 namespace WebCore {
 
-template <typename LayoutType>
-static std::pair<std::optional<unsigned>, std::optional<unsigned>> indicesOfNearestSnapOffsets(LayoutType offset, const Vector<SnapOffset<LayoutType>>& snapOffsets)
-{
-    unsigned lowerIndex = 0;
-    unsigned upperIndex = snapOffsets.size() - 1;
-    while (lowerIndex < upperIndex - 1) {
-        int middleIndex = (lowerIndex + upperIndex) / 2;
-        auto middleOffset = snapOffsets[middleIndex].offset;
-        if (offset == middleOffset) {
-            upperIndex = middleIndex;
-            lowerIndex = middleIndex;
-            break;
-        }
+template <typename UnitType>
+struct PotentialSnapPointSearchResult {
+    std::optional<std::pair<UnitType, unsigned>> previous;
+    std::optional<std::pair<UnitType, unsigned>> next;
+    std::optional<std::pair<UnitType, unsigned>> snapStop;
+};
 
-        if (offset > middleOffset)
-            lowerIndex = middleIndex;
-        else
-            upperIndex = middleIndex;
-    }
-
-    return std::make_pair(lowerIndex, upperIndex);
-}
-
-template <typename LayoutType>
-static std::optional<unsigned> findFirstSnapStopOffsetBetweenOriginAndDestination(const Vector<SnapOffset<LayoutType>>& snapOffsets, LayoutType scrollOriginOffset, LayoutType scrollDestinationOffset)
+template <typename InfoType, typename UnitType>
+static PotentialSnapPointSearchResult<UnitType> searchForPotentialSnapPoints(const InfoType& info, ScrollEventAxis axis, UnitType destinationOffset, std::optional<UnitType> originalOffset)
 {
-    LayoutType difference = scrollDestinationOffset - scrollOriginOffset;
-    if (!difference)
-        return std::nullopt;
+    const auto& snapOffsets = info.offsetsForAxis(axis);
+    std::optional<std::pair<UnitType, unsigned>> previous, next, exact, snapStop;
 
-    unsigned searchStartOffset = 0;
-    size_t iteration = 1;
-    if (difference < 0) {
-        searchStartOffset = snapOffsets.size() - 1;
-        iteration = -1;
-    }
-
-    auto isPast = [difference](LayoutType mark, LayoutType candidate) {
-        return (difference > 0 && candidate > mark) || (difference < 0 && candidate < mark);
+    // A particular snap stop is better if it's between the original offset and destination offset and closer original
+    // offset than the previously selected snap stop. We always want to stop at the snap stop closest to the original offset.
+    auto isBetterSnapStop = [&](UnitType candidate) {
+        if (!originalOffset)
+            return false;
+        auto original = *originalOffset;
+        if (candidate <= std::min(destinationOffset, original) || candidate >= std::max(destinationOffset, original))
+            return false;
+        return !snapStop || std::abs(float { candidate - original }) < std::abs(float { (*snapStop).first - original });
     };
 
-    for (int i = searchStartOffset; i >= 0 && static_cast<size_t>(i) < snapOffsets.size(); i += iteration) {
-        auto offset = snapOffsets[i].offset;
-        if (isPast(scrollDestinationOffset, offset))
-            break;
-        if (snapOffsets[i].stop != ScrollSnapStop::Always)
-            continue;
-        if (isPast(scrollOriginOffset, offset))
-            return i;
+    for (unsigned i = 0; i < snapOffsets.size(); i++) {
+        UnitType potentialSnapOffset = snapOffsets[i].offset;
+        if (potentialSnapOffset == destinationOffset)
+            exact = std::make_pair(potentialSnapOffset, i);
+        else if (potentialSnapOffset < destinationOffset)
+            previous = std::make_pair(potentialSnapOffset, i);
+        else if (!next && potentialSnapOffset > destinationOffset)
+            next = std::make_pair(potentialSnapOffset, i);
+
+        if (snapOffsets[i].stop == ScrollSnapStop::Always && isBetterSnapStop(potentialSnapOffset))
+            snapStop = std::make_pair(potentialSnapOffset, i);
     }
 
-    return std::nullopt;
+    if (exact)
+        return { exact, exact, snapStop };
+    return { previous, next, snapStop };
 }
 
 template <typename InfoType, typename SizeType, typename LayoutType>
@@ -102,10 +90,9 @@
     if (snapOffsets.isEmpty())
         return pairForNoSnapping;
 
-    if (originalOffsetForDirectionalSnapping) {
-        if (auto firstSnapStopOffsetIndex = findFirstSnapStopOffsetBetweenOriginAndDestination(snapOffsets, *originalOffsetForDirectionalSnapping, scrollDestinationOffset))
-            return std::make_pair(snapOffsets[*firstSnapStopOffsetIndex].offset, firstSnapStopOffsetIndex);
-    }
+    auto [previous, next, snapStop] = searchForPotentialSnapPoints(info, axis, scrollDestinationOffset, originalOffsetForDirectionalSnapping);
+    if (snapStop)
+        return *snapStop;
 
     auto viewportLength = axis == ScrollEventAxis::Horizontal ? viewportSize.width() : viewportSize.height();
     auto isNearEnoughToOffsetForProximity = [&](LayoutType candidateSnapOffset) {
@@ -126,41 +113,37 @@
         return isNearEnoughToOffsetForProximity(snapOffsets.last().offset) ? std::make_pair(snapOffsets.last().offset, std::make_optional(lastIndex)) : pairForNoSnapping;
     }
 
-    auto [lowerIndex, upperIndex] = indicesOfNearestSnapOffsets<LayoutType>(scrollDestinationOffset, snapOffsets);
-    ASSERT(lowerIndex);
-    ASSERT(upperIndex);
-    LayoutType lowerSnapPosition = snapOffsets[*lowerIndex].offset;
-    LayoutType upperSnapPosition = snapOffsets[*upperIndex].offset;
+    if (previous && !isNearEnoughToOffsetForProximity((*previous).first))
+        previous.reset();
+    if (next && !isNearEnoughToOffsetForProximity((*next).first))
+        next.reset();
 
-    if (!isNearEnoughToOffsetForProximity(lowerSnapPosition)) {
-        lowerSnapPosition = scrollDestinationOffset;
-        lowerIndex.reset();
+    if (originalOffsetForDirectionalSnapping) {
+        // From https://www.w3.org/TR/css-scroll-snap-1/#choosing
+        // "User agents must ensure that a user can “escape” a snap position, regardless of the scroll
+        // method. For example, if the snap type is mandatory and the next snap position is more than
+        // two screen-widths away, a naïve “always snap to nearest” selection algorithm might “trap” the
+        //
+        // For a directional scroll, we never snap back to the original scroll position or before it,
+        // always preferring the snap offset in the scroll direction.
+        auto& originalOffset = *originalOffsetForDirectionalSnapping;
+        if (originalOffset < scrollDestinationOffset && previous && (*previous).first <= originalOffset)
+            previous.reset();
+        if (originalOffset > scrollDestinationOffset && next && (*next).first >= originalOffset)
+            next.reset();
     }
 
-    if (!isNearEnoughToOffsetForProximity(upperSnapPosition)) {
-        upperSnapPosition = scrollDestinationOffset;
-        upperIndex.reset();
+    if (!previous && !next)
+        return pairForNoSnapping;
+    if (!previous)
+        return *next;
+    if (!next)
+        return *previous;
 
-    }
-
-    if (!std::abs(velocity)) {
-        bool isCloserToLowerSnapPosition = !upperIndex || (lowerIndex && scrollDestinationOffset - lowerSnapPosition <= upperSnapPosition - scrollDestinationOffset);
-        return isCloserToLowerSnapPosition ? std::make_pair(lowerSnapPosition, lowerIndex) : std::make_pair(upperSnapPosition, upperIndex);
-    }
-
-    // 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 is close enough for proximity (or we aren't using proximity). If we are doing directional
-    // snapping, we should never snap to a point that was on the other side of the original position in the opposite direction of this scroll.
-    // This allows directional scrolling to escape snap points.
-    if (velocity < 0)  {
-        if (upperIndex && (!originalOffsetForDirectionalSnapping || *originalOffsetForDirectionalSnapping > upperSnapPosition))
-            return std::make_pair(upperSnapPosition, upperIndex);
-        return std::make_pair(lowerSnapPosition, lowerIndex);
-    }
-
-    if (lowerIndex && (!originalOffsetForDirectionalSnapping || *originalOffsetForDirectionalSnapping < lowerSnapPosition))
-        return std::make_pair(lowerSnapPosition, lowerIndex);
-    return std::make_pair(upperSnapPosition, upperIndex);
+    // If this scroll isn't directional, then choose whatever snap point is closer, otherwise pick the offset in the scroll direction.
+    if (!std::abs(velocity))
+        return (scrollDestinationOffset - (*previous).first) <= ((*next).first - scrollDestinationOffset) ? *previous : *next;
+    return velocity < 0 ? *previous : *next;
 }
 
 enum class InsetOrOutset {
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to