diff --git a/layout/generic/ScrollSnap.cpp b/layout/generic/ScrollSnap.cpp index 76e2e8fa38f6..2ae7fb3e1a73 100644 --- a/layout/generic/ScrollSnap.cpp +++ b/layout/generic/ScrollSnap.cpp @@ -25,19 +25,34 @@ class CalcSnapPoints final { public: CalcSnapPoints(ScrollUnit aUnit, ScrollSnapFlags aSnapFlags, const nsPoint& aDestination, const nsPoint& aStartPos); - void AddHorizontalEdge(nscoord aEdge); - void AddVerticalEdge(nscoord aEdge); + struct SnapPosition { + SnapPosition() = default; + + SnapPosition(nscoord aPosition, StyleScrollSnapStop aScrollSnapStop) + : mPosition(aPosition), mScrollSnapStop(aScrollSnapStop) {} + + nscoord mPosition; + StyleScrollSnapStop mScrollSnapStop; + }; + + void AddHorizontalEdge(const SnapPosition& aEdge); + void AddVerticalEdge(const SnapPosition& aEdge); + void AddEdge(const SnapPosition& aEdge, nscoord aDestination, + nscoord aStartPos, nscoord aScrollingDirection, + SnapPosition* aBestEdge, SnapPosition* aSecondBestEdge, + bool* aEdgeFound); + void AddEdge(nscoord aEdge, nscoord aDestination, nscoord aStartPos, nscoord aScrollingDirection, nscoord* aBestEdge, nscoord* aSecondBestEdge, bool* aEdgeFound); nsPoint GetBestEdge() const; nscoord XDistanceBetweenBestAndSecondEdge() const { - return std::abs( - NSCoordSaturatingSubtract(mSecondBestEdge.x, mBestEdge.x, nscoord_MAX)); + return std::abs(NSCoordSaturatingSubtract( + mSecondBestEdgeX.mPosition, mBestEdgeX.mPosition, nscoord_MAX)); } nscoord YDistanceBetweenBestAndSecondEdge() const { - return std::abs( - NSCoordSaturatingSubtract(mSecondBestEdge.y, mBestEdge.y, nscoord_MAX)); + return std::abs(NSCoordSaturatingSubtract( + mSecondBestEdgeY.mPosition, mBestEdgeY.mPosition, nscoord_MAX)); } const nsPoint& Destination() const { return mDestination; } @@ -48,14 +63,20 @@ class CalcSnapPoints final { // snapping nsPoint mStartPos; // gives the position before scrolling nsIntPoint mScrollingDirection; // always -1, 0, or 1 - nsPoint mBestEdge; // keeps track of the position of the current best edge - nsPoint mSecondBestEdge; // keeps track of the position of the current - // second best edge on the opposite side of the best + SnapPosition mBestEdgeX; // keeps track of the position of the current best + // edge on X axis + SnapPosition mBestEdgeY; // keeps track of the position of the current best + // edge on Y axis + SnapPosition mSecondBestEdgeX; // keeps track of the position of the current + // second best edge on the opposite side of + // the best edge on X axis + SnapPosition mSecondBestEdgeY; // keeps track of the position of the current + // second best edge on the opposite side of + // the best edge on Y axis + bool mHorizontalEdgeFound; // true if mBestEdge.x is storing a valid + // horizontal edge + bool mVerticalEdgeFound; // true if mBestEdge.y is storing a valid vertical // edge - bool mHorizontalEdgeFound; // true if mBestEdge.x is storing a valid - // horizontal edge - bool mVerticalEdgeFound; // true if mBestEdge.y is storing a valid vertical - // edge }; CalcSnapPoints::CalcSnapPoints(ScrollUnit aUnit, ScrollSnapFlags aSnapFlags, @@ -82,40 +103,42 @@ CalcSnapPoints::CalcSnapPoints(ScrollUnit aUnit, ScrollSnapFlags aSnapFlags, if (direction.y > 0) { mScrollingDirection.y = 1; } - mBestEdge = aDestination; + mBestEdgeX = SnapPosition{aDestination.x, StyleScrollSnapStop::Normal}; + mBestEdgeY = SnapPosition{aDestination.y, StyleScrollSnapStop::Normal}; // We use NSCoordSaturatingSubtract to calculate the distance between a given // position and this second best edge position so that it can be an // uninitialized value as the maximum possible value, because the first // distance calculation would always be nscoord_MAX. - mSecondBestEdge = nsPoint(nscoord_MAX, nscoord_MAX); + mSecondBestEdgeX = SnapPosition{nscoord_MAX, StyleScrollSnapStop::Normal}; + mSecondBestEdgeY = SnapPosition{nscoord_MAX, StyleScrollSnapStop::Normal}; mHorizontalEdgeFound = false; mVerticalEdgeFound = false; } nsPoint CalcSnapPoints::GetBestEdge() const { - return nsPoint(mVerticalEdgeFound ? mBestEdge.x : mStartPos.x, - mHorizontalEdgeFound ? mBestEdge.y : mStartPos.y); + return nsPoint(mVerticalEdgeFound ? mBestEdgeX.mPosition : mStartPos.x, + mHorizontalEdgeFound ? mBestEdgeY.mPosition : mStartPos.y); } -void CalcSnapPoints::AddHorizontalEdge(nscoord aEdge) { +void CalcSnapPoints::AddHorizontalEdge(const SnapPosition& aEdge) { AddEdge(aEdge, mDestination.y, mStartPos.y, mScrollingDirection.y, - &mBestEdge.y, &mSecondBestEdge.y, &mHorizontalEdgeFound); + &mBestEdgeY, &mSecondBestEdgeY, &mHorizontalEdgeFound); } -void CalcSnapPoints::AddVerticalEdge(nscoord aEdge) { +void CalcSnapPoints::AddVerticalEdge(const SnapPosition& aEdge) { AddEdge(aEdge, mDestination.x, mStartPos.x, mScrollingDirection.x, - &mBestEdge.x, &mSecondBestEdge.x, &mVerticalEdgeFound); + &mBestEdgeX, &mSecondBestEdgeX, &mVerticalEdgeFound); } -void CalcSnapPoints::AddEdge(nscoord aEdge, nscoord aDestination, +void CalcSnapPoints::AddEdge(const SnapPosition& aEdge, nscoord aDestination, nscoord aStartPos, nscoord aScrollingDirection, - nscoord* aBestEdge, nscoord* aSecondBestEdge, - bool* aEdgeFound) { + SnapPosition* aBestEdge, + SnapPosition* aSecondBestEdge, bool* aEdgeFound) { if (mSnapFlags & ScrollSnapFlags::IntendedDirection) { // In the case of intended direction, we only want to snap to points ahead // of the direction we are scrolling. if (aScrollingDirection == 0 || - (aEdge - aStartPos) * aScrollingDirection <= 0) { + (aEdge.mPosition - aStartPos) * aScrollingDirection <= 0) { // The scroll direction is neutral - will not hit a snap point, or the // edge is not in the direction we are scrolling, skip it. return; @@ -128,19 +151,52 @@ void CalcSnapPoints::AddEdge(nscoord aEdge, nscoord aDestination, return; } - const bool isOnOppositeSide = - ((aEdge - aDestination) > 0) != ((*aBestEdge - aDestination) > 0); + auto isPreferredStopAlways = [&](const SnapPosition& aSnapPosition) -> bool { + MOZ_ASSERT(mSnapFlags & ScrollSnapFlags::IntendedDirection); + // In the case of intended direction scroll operations, `scroll-snap-stop: + // always` snap points in between the start point and the scroll destination + // are preferable preferable. In other words any `scroll-snap-stop: always` + // snap points can be handled as if it's `scroll-snap-stop: normal`. + return aSnapPosition.mScrollSnapStop == StyleScrollSnapStop::Always && + std::abs(aSnapPosition.mPosition - aStartPos) < + std::abs(aDestination - aStartPos); + }; + + const bool isOnOppositeSide = ((aEdge.mPosition - aDestination) > 0) != + ((aBestEdge->mPosition - aDestination) > 0); + const nscoord distanceFromStart = aEdge.mPosition - aStartPos; // A utility function to update the best and the second best edges in the // given conditions. // |aIsCloserThanBest| True if the current candidate is closer than the best // edge. // |aIsCloserThanSecond| True if the current candidate is closer than // the second best edge. + const nscoord distanceFromDestination = aEdge.mPosition - aDestination; auto updateBestEdges = [&](bool aIsCloserThanBest, bool aIsCloserThanSecond) { if (aIsCloserThanBest) { - // Replace the second best edge with the current best edge only if the new - // best edge (aEdge) is on the opposite side of the current best edge. - if (isOnOppositeSide) { + if (mSnapFlags & ScrollSnapFlags::IntendedDirection && + isPreferredStopAlways(aEdge)) { + // In the case of intended direction scroll operations and the new best + // candidate is `scroll-snap-stop: always` and if it's closer to the + // start position than the destination, thus we won't use the second + // best edge since even if the snap port of the best edge covers entire + // snapport, the `scroll-snap-stop: always` snap point is preferred than + // any points. + // NOTE: We've already ignored snap points behind start points so that + // we can use std::abs here in the comparison. + // + // For example, if there's a `scroll-snap-stop: always` in between the + // start point and destination, no `snap-overflow` mechanism should + // happen, if there's `scroll-snap-stop: always` further than the + // destination, `snap-overflow` might happen something like below + // diagram. + // start always dest other always + // |------------|---------|------| + *aSecondBestEdge = aEdge; + } else if (isOnOppositeSide) { + // Replace the second best edge with the current best edge only if the + // new best edge (aEdge) is on the opposite side of the current best + // edge. *aSecondBestEdge = *aBestEdge; } *aBestEdge = aEdge; @@ -157,23 +213,25 @@ void CalcSnapPoints::AddEdge(nscoord aEdge, nscoord aDestination, case ScrollUnit::DEVICE_PIXELS: case ScrollUnit::LINES: case ScrollUnit::WHOLE: { - nscoord distance = std::abs(aEdge - aDestination); - isCandidateOfBest = distance < std::abs(*aBestEdge - aDestination); + isCandidateOfBest = std::abs(distanceFromDestination) < + std::abs(aBestEdge->mPosition - aDestination); isCandidateOfSecondBest = - distance < std::abs(NSCoordSaturatingSubtract( - *aSecondBestEdge, aDestination, nscoord_MAX)); + std::abs(distanceFromDestination) < + std::abs(NSCoordSaturatingSubtract(aSecondBestEdge->mPosition, + aDestination, nscoord_MAX)); break; } case ScrollUnit::PAGES: { // distance to the edge from the scrolling destination in the direction of // scrolling - nscoord overshoot = (aEdge - aDestination) * aScrollingDirection; + nscoord overshoot = distanceFromDestination * aScrollingDirection; // distance to the current best edge from the scrolling destination in the // direction of scrolling - nscoord curOvershoot = (*aBestEdge - aDestination) * aScrollingDirection; + nscoord curOvershoot = + (aBestEdge->mPosition - aDestination) * aScrollingDirection; nscoord secondOvershoot = - NSCoordSaturatingSubtract(*aSecondBestEdge, aDestination, + NSCoordSaturatingSubtract(aSecondBestEdge->mPosition, aDestination, nscoord_MAX) * aScrollingDirection; @@ -193,6 +251,21 @@ void CalcSnapPoints::AddEdge(nscoord aEdge, nscoord aDestination, } } + if (mSnapFlags & ScrollSnapFlags::IntendedDirection) { + if (isPreferredStopAlways(aEdge)) { + // If the given position is `scroll-snap-stop: always` and if the position + // is in between the start and the destination positions, update the best + // position based on the distance from the __start__ point. + isCandidateOfBest = std::abs(distanceFromStart) < + std::abs(aBestEdge->mPosition - aStartPos); + } else if (isPreferredStopAlways(*aBestEdge)) { + // If we've found a preferable `scroll-snap-stop:always` position as the + // best, do not update it unless the given position is also + // `scroll-snap-stop: always`. + isCandidateOfBest = false; + } + } + updateBestEdges(isCandidateOfBest, isCandidateOfSecondBest); } @@ -213,11 +286,13 @@ static void ProcessSnapPositions(CalcSnapPoints& aCalcSnapPoints, if (target.mSnapPositionX && aSnapInfo.mScrollSnapStrictnessX != StyleScrollSnapStrictness::None) { - aCalcSnapPoints.AddVerticalEdge(*target.mSnapPositionX); + aCalcSnapPoints.AddVerticalEdge( + {*target.mSnapPositionX, target.mScrollSnapStop}); } if (target.mSnapPositionY && aSnapInfo.mScrollSnapStrictnessY != StyleScrollSnapStrictness::None) { - aCalcSnapPoints.AddHorizontalEdge(*target.mSnapPositionY); + aCalcSnapPoints.AddHorizontalEdge( + {*target.mSnapPositionY, target.mScrollSnapStop}); } } } @@ -251,7 +326,8 @@ Maybe ScrollSnapUtils::GetSnapPointForDestination( if (range.IsValid(clampedDestination.x, aSnapInfo.mSnapportSize.width) && calcSnapPoints.XDistanceBetweenBestAndSecondEdge() > aSnapInfo.mSnapportSize.width) { - calcSnapPoints.AddVerticalEdge(clampedDestination.x); + calcSnapPoints.AddVerticalEdge(CalcSnapPoints::SnapPosition{ + clampedDestination.x, StyleScrollSnapStop::Normal}); break; } } @@ -259,7 +335,8 @@ Maybe ScrollSnapUtils::GetSnapPointForDestination( if (range.IsValid(clampedDestination.y, aSnapInfo.mSnapportSize.height) && calcSnapPoints.YDistanceBetweenBestAndSecondEdge() > aSnapInfo.mSnapportSize.height) { - calcSnapPoints.AddHorizontalEdge(clampedDestination.y); + calcSnapPoints.AddHorizontalEdge(CalcSnapPoints::SnapPosition{ + clampedDestination.y, StyleScrollSnapStop::Normal}); break; } } diff --git a/testing/web-platform/meta/css/css-scroll-snap/scroll-snap-stop-change.html.ini b/testing/web-platform/meta/css/css-scroll-snap/scroll-snap-stop-change.html.ini deleted file mode 100644 index a7950c4d2084..000000000000 --- a/testing/web-platform/meta/css/css-scroll-snap/scroll-snap-stop-change.html.ini +++ /dev/null @@ -1,7 +0,0 @@ -[scroll-snap-stop-change.html] - [scroll-snap-stop for areas on HTML should control snapping behavior and changing it takes effect] - expected: FAIL - - [scroll-snap-stop for areas on DIV should control snapping behavior and changing it takes effect] - expected: FAIL - diff --git a/testing/web-platform/meta/css/css-scroll-snap/scroll-snap-stop.html.ini b/testing/web-platform/meta/css/css-scroll-snap/scroll-snap-stop.html.ini deleted file mode 100644 index 5f7b0f6085ab..000000000000 --- a/testing/web-platform/meta/css/css-scroll-snap/scroll-snap-stop.html.ini +++ /dev/null @@ -1,9 +0,0 @@ -[scroll-snap-stop.html] - [A scroll outside bounds in the non-snapping axis with intended direction and end position should not pass a snap area with scroll-snap-stop: always.] - expected: FAIL - - [A scroll outside bounds in the snapping axis with intended direction and end position should not pass a snap area with scroll-snap-stop: always.] - expected: FAIL - - [A scroll with intended direction and end position should not pass a snap area with scroll-snap-stop: always.] - expected: FAIL diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop-001.html similarity index 100% rename from testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop.html rename to testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop-001.html diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop-002.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop-002.html new file mode 100644 index 000000000000..cadafe2a82de --- /dev/null +++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop-002.html @@ -0,0 +1,238 @@ + + + + + + + + +
+
+
+ +
+ +
+
+ + +
+
+
+
+ +
+
+ + + +
+
+
+
+ +
+ +
+
+ + +
+
+
+
+ +
+ +
+
+ + +
+
+
+
+ +
+ +
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+ +