Bug 1312165 - Honor `scroll-snap-stop: always`. r=botond

Depends on D145851

Differential Revision: https://phabricator.services.mozilla.com/D145852
This commit is contained in:
Hiroyuki Ikezoe 2022-05-31 00:45:12 +00:00
Родитель 17647a8cc5
Коммит d2f6bd5b5c
5 изменённых файлов: 356 добавлений и 57 удалений

Просмотреть файл

@ -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<nsPoint> 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<nsPoint> 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;
}
}

Просмотреть файл

@ -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

Просмотреть файл

@ -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

Просмотреть файл

@ -0,0 +1,238 @@
<!DOCTYPE html>
<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#scroll-snap-stop" />
<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#snap-overflow" />
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<style>
div {
position: absolute;
}
.scroller {
width: 400px;
height: 100px;
overflow: scroll;
scroll-snap-type: x mandatory;
}
#space {
left: 0px;
top: 0px;
width: 2100px;
height: 50px;
}
.target {
width: 50px;
height: 50px;
scroll-snap-align: start;
top: 0px;
}
</style>
<!--
start dest closest always
|------------------------------|--------|--------|
-->
<div class="scroller" id="scroller1">
<div id="space"></div>
<div class="target" style="left: 0px;"></div>
<!-- Add `scroll-snap-stop: always` element into the DOM tree prior to the
closest snap point -->
<div class="target" style="left: 120px; scroll-snap-stop: always;"></div>
<!-- Add a snap point closest-to-destination but further than the destination
from the start position -->
<div class="target" style="left: 110px;"></div>
</div>
<!--
start closest dest always
|------------------------------|------|--------|
-->
<div class="scroller" id="scroller2">
<div id="space"></div>
<div class="target" style="left: 0px;"></div>
<div class="target" style="left: 120px; scroll-snap-stop: always;"></div>
<!-- Add a snap point closest-to-destination and closer than the destination
from the start position -->
<div class="target" style="left: 95px;"></div>
</div>
</div>
<!--
A test case where there's a snap point whose snap area covers the snapport and
there's a `scroll-snap-stop: always` snap point on the opposite side.
-->
<div class="scroller" id="scroller3">
<div id="space"></div>
<div class="target" style="left: 0px;"></div>
<div class="target" style="left: 700px; scroll-snap-stop: always;"></div>
<!-- Add a snap point where the snap area covers the snapport entirely -->
<div class="target" style="left: 100px; width: 500px;"></div>
<!-- Add a snap point where the distance between this and the 100px point is
larger than the snapport size (400px) -->
<div class="target" style="left: 600px;"></div>
</div>
<!--
A similar case above, but two `scroll-snap-stop: always` snap points.
-->
<div class="scroller" id="scroller4">
<div id="space"></div>
<div class="target" style="left: 0px;"></div>
<div class="target" style="left: 700px; scroll-snap-stop: always;"></div>
<!-- Add a snap point where the snap area covers the snapport entirely -->
<div class="target" style="left: 100px; width: 500px;"></div>
<!-- Add a snap point where the distance between this and the 100px point is
larger than the snapport size (400px) -->
<div class="target" style="left: 600px; scroll-snap-stop: always;"></div>
</div>
<!--
Another similar case, but the nearest snap point to the start point is
`scroll-snap-stop: always`.
-->
<div class="scroller" id="scroller5">
<div id="space"></div>
<div class="target" style="left: 0px;"></div>
<div class="target" style="left: 700px; scroll-snap-stop: always;"></div>
<!-- Add a snap point where the snap area covers the snapport entirely -->
<div class="target" style="left: 100px; width: 500px; scroll-snap-stop: always;"></div>
<!-- Add a snap point where the distance between this and the 100px point is
larger than the snapport size (400px) -->
<div class="target" style="left: 600px;"></div>
</div>
<!--
A test case that a `scroll-snap-stop: always` snap point is further than the
scroll destination.
-->
<div class="scroller" id="scroller6">
<div id="space"></div>
<div class="target" style="left: 0px;"></div>
<div class="target" style="left: 400px;"></div>
<div class="target" style="left: 700px; scroll-snap-stop: always;"></div
</div>
<!--
Similar to above but a snap area covers the snapport.
-->
<div class="scroller" id="scroller7">
<div id="space"></div>
<div class="target" style="left: 0px;"></div>
<div class="target" style="left: 400px; width: 900px;"></div>
<div class="target" style="left: 900px; scroll-snap-stop: always;"></div
</div>
<script>
test(() => {
assert_equals(scroller1.scrollLeft, 0);
assert_equals(scroller1.scrollTop, 0);
// start dest closest always
// |------------------------------|--------|--------|
// 0 100 110 120
scroller1.scrollBy(100, 0);
assert_equals(scroller1.scrollLeft, 110);
assert_equals(scroller1.scrollTop, 0);
}, "The closest snap point is preferred than scroll-snap-stop: always where " +
"it's further than the destination (the closest one is closer to the " +
"scroll start position than the destination)");
test(() => {
assert_equals(scroller2.scrollLeft, 0);
assert_equals(scroller2.scrollTop, 0);
// start closest dest always
// |------------------------------|------|--------|
// 0 95 100 120
scroller2.scrollBy(100, 0);
assert_equals(scroller2.scrollLeft, 95);
assert_equals(scroller2.scrollTop, 0);
}, "The closest snap point is preferred than scroll-snap-stop: always where " +
"it's further than the destination (the closest one is futrher than the " +
"destination from the start position)");
test(() => {
assert_equals(scroller3.scrollLeft, 0);
assert_equals(scroller3.scrollTop, 0);
// start dest always
// |-----|===|============================|------|
// 0 100 150 600 700
// Scoll on the element whose snap area is larger than the snapport.
scroller3.scrollBy(150, 0);
assert_equals(scroller3.scrollLeft, 150);
assert_equals(scroller3.scrollTop, 0);
}, "The scroll destination on a large element whose snap area covers " +
"the snapport entirely is a valid snap position");
test(() => {
assert_equals(scroller4.scrollLeft, 0);
assert_equals(scroller4.scrollTop, 0);
// start dest always always
// |-----|====|============================|------|
// 0 100 150 600 700
// Scoll on the element whose snap area is larger than the snapport.
scroller4.scrollBy(150, 0);
assert_equals(scroller4.scrollLeft, 150);
assert_equals(scroller4.scrollTop, 0);
}, "The scroll destination on a large element whose snap area covers " +
"the snapport entirely is a valid snap position (with two " +
"`scroll-snap-stop: always` snap points");
test(() => {
assert_equals(scroller5.scrollLeft, 0);
assert_equals(scroller5.scrollTop, 0);
// start always dest always
// |-----|=====|============================|------|
// 0 100 150 600 700
// Scoll on the element whose snap area is larger than the snapport, but
// the scroll-snap-stop property is `always`.
scroller5.scrollBy(150, 0);
assert_equals(scroller5.scrollLeft, 100);
assert_equals(scroller5.scrollTop, 0);
// Scroll the direction further, it should NOT be trapped by the
// `scroll-snap-stop: always` snap point.
scroller5.scrollBy(50, 0);
assert_equals(scroller5.scrollLeft, 150);
assert_equals(scroller5.scrollTop, 0);
}, "`scroll-snap-stop: always` snap point is preferred even if the snap area " +
"entire snapport");
test(() => {
assert_equals(scroller6.scrollLeft, 0);
assert_equals(scroller6.scrollTop, 0);
// start dest always
// |-------------------------|-----------------|------|
// 0 400 600 700
// Scroll to a point where it's closer to a `scroll-snap-stop: always` snap
// position.
scroller6.scrollBy(600, 0);
assert_equals(scroller6.scrollLeft, 700);
assert_equals(scroller6.scrollTop, 0);
}, "`scroll-snap-stop: always` snap point is further than the scroll " +
"destination");
test(() => {
assert_equals(scroller7.scrollLeft, 0);
assert_equals(scroller7.scrollTop, 0);
// start dest always
// |-------------------------|================|=====|=====
// 0 400 700 900
// Scoll on the element whose snap area is larger than the snapport.
scroller7.scrollBy(700, 0);
assert_equals(scroller7.scrollLeft, 700);
assert_equals(scroller7.scrollTop, 0);
}, "`scroll-snap-stop: always` snap point is further than the scroll " +
"destination and a snap area covers the snapport");
</script>