gecko-dev/layout/generic/ScrollSnap.cpp

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

279 строки
11 KiB
C++
Исходник Обычный вид История

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "ScrollSnap.h"
#include "FrameMetrics.h"
#include "mozilla/Maybe.h"
#include "mozilla/Preferences.h"
#include "mozilla/StaticPrefs_layout.h"
#include "nsLineLayout.h"
namespace mozilla {
using layers::ScrollSnapInfo;
/**
* Keeps track of the current best edge to snap to. The criteria for
* adding an edge depends on the scrolling unit.
*/
class CalcSnapPoints final {
public:
CalcSnapPoints(ScrollUnit aUnit, ScrollSnapFlags aSnapFlags,
const nsPoint& aDestination, const nsPoint& aStartPos);
void AddHorizontalEdge(nscoord aEdge);
void AddVerticalEdge(nscoord aEdge);
void AddEdge(nscoord aEdge, nscoord aDestination, nscoord aStartPos,
nscoord aScrollingDirection, nscoord* aBestEdge,
nscoord* aSecondBestEdge, bool* aEdgeFound);
nsPoint GetBestEdge() const;
nscoord XDistanceBetweenBestAndSecondEdge() const {
Bug 1766192 - Choose the second best edge on the opposite side of the best edge. r=botond The second best edge is used for `snap-overflow` feature [1]. The `snap-overflow` is applied when the following conditions are met. 1) A scroll snap target element is larger than the snapport (e.g. scrollport) in an axis 2) The distance between the best edge and the second best edge (on the opposite side of the best edge) is later than the snapport size in the axis There was a problem in our implementation. For example, in below diagram, D is the original destination of the given scroll operation, 1 is the best edge, 2 is the second best in our original implementation and 3 is of of the other snap points. In this case if there's an element larger than the distance between 1 and 3 and if the snapport size is larger than the distance between 1 and 2 but smaller than the distance between 1 and 3, we should apply `snap-overflow`. 2 1 D 3 |-|---|-----| There are a couple of test cases in wpt for this condition, for example there's one of them in oveflowing-snap-area.html [2]. That test case have been passed incorrectly due to our incorrect `snap-scope` implementation which will be fixed a subsequent change in this commit series. [1] https://drafts.csswg.org/css-scroll-snap-1/#snap-overflow [2] https://searchfox.org/mozilla-central/rev/13d69189a8abfc5064fe44944550b9b6eb4544f5/testing/web-platform/tests/css/css-scroll-snap/overflowing-snap-areas.html#131-137 Differential Revision: https://phabricator.services.mozilla.com/D144531
2022-05-10 11:51:37 +03:00
return std::abs(
NSCoordSaturatingSubtract(mSecondBestEdge.x, mBestEdge.x, nscoord_MAX));
}
nscoord YDistanceBetweenBestAndSecondEdge() const {
Bug 1766192 - Choose the second best edge on the opposite side of the best edge. r=botond The second best edge is used for `snap-overflow` feature [1]. The `snap-overflow` is applied when the following conditions are met. 1) A scroll snap target element is larger than the snapport (e.g. scrollport) in an axis 2) The distance between the best edge and the second best edge (on the opposite side of the best edge) is later than the snapport size in the axis There was a problem in our implementation. For example, in below diagram, D is the original destination of the given scroll operation, 1 is the best edge, 2 is the second best in our original implementation and 3 is of of the other snap points. In this case if there's an element larger than the distance between 1 and 3 and if the snapport size is larger than the distance between 1 and 2 but smaller than the distance between 1 and 3, we should apply `snap-overflow`. 2 1 D 3 |-|---|-----| There are a couple of test cases in wpt for this condition, for example there's one of them in oveflowing-snap-area.html [2]. That test case have been passed incorrectly due to our incorrect `snap-scope` implementation which will be fixed a subsequent change in this commit series. [1] https://drafts.csswg.org/css-scroll-snap-1/#snap-overflow [2] https://searchfox.org/mozilla-central/rev/13d69189a8abfc5064fe44944550b9b6eb4544f5/testing/web-platform/tests/css/css-scroll-snap/overflowing-snap-areas.html#131-137 Differential Revision: https://phabricator.services.mozilla.com/D144531
2022-05-10 11:51:37 +03:00
return std::abs(
NSCoordSaturatingSubtract(mSecondBestEdge.y, mBestEdge.y, nscoord_MAX));
}
const nsPoint& Destination() const { return mDestination; }
protected:
ScrollUnit mUnit;
ScrollSnapFlags mSnapFlags;
nsPoint mDestination; // gives the position after scrolling but before
// 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
Bug 1766192 - Choose the second best edge on the opposite side of the best edge. r=botond The second best edge is used for `snap-overflow` feature [1]. The `snap-overflow` is applied when the following conditions are met. 1) A scroll snap target element is larger than the snapport (e.g. scrollport) in an axis 2) The distance between the best edge and the second best edge (on the opposite side of the best edge) is later than the snapport size in the axis There was a problem in our implementation. For example, in below diagram, D is the original destination of the given scroll operation, 1 is the best edge, 2 is the second best in our original implementation and 3 is of of the other snap points. In this case if there's an element larger than the distance between 1 and 3 and if the snapport size is larger than the distance between 1 and 2 but smaller than the distance between 1 and 3, we should apply `snap-overflow`. 2 1 D 3 |-|---|-----| There are a couple of test cases in wpt for this condition, for example there's one of them in oveflowing-snap-area.html [2]. That test case have been passed incorrectly due to our incorrect `snap-scope` implementation which will be fixed a subsequent change in this commit series. [1] https://drafts.csswg.org/css-scroll-snap-1/#snap-overflow [2] https://searchfox.org/mozilla-central/rev/13d69189a8abfc5064fe44944550b9b6eb4544f5/testing/web-platform/tests/css/css-scroll-snap/overflowing-snap-areas.html#131-137 Differential Revision: https://phabricator.services.mozilla.com/D144531
2022-05-10 11:51:37 +03:00
nsPoint mSecondBestEdge; // keeps track of the position of the current
// second best edge on the opposite side of the best
// 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,
const nsPoint& aDestination,
const nsPoint& aStartPos) {
MOZ_ASSERT(aSnapFlags != ScrollSnapFlags::Disabled);
mUnit = aUnit;
mSnapFlags = aSnapFlags;
mDestination = aDestination;
mStartPos = aStartPos;
nsPoint direction = aDestination - aStartPos;
mScrollingDirection = nsIntPoint(0, 0);
if (direction.x < 0) {
mScrollingDirection.x = -1;
}
if (direction.x > 0) {
mScrollingDirection.x = 1;
}
if (direction.y < 0) {
mScrollingDirection.y = -1;
}
if (direction.y > 0) {
mScrollingDirection.y = 1;
}
mBestEdge = aDestination;
Bug 1766192 - Choose the second best edge on the opposite side of the best edge. r=botond The second best edge is used for `snap-overflow` feature [1]. The `snap-overflow` is applied when the following conditions are met. 1) A scroll snap target element is larger than the snapport (e.g. scrollport) in an axis 2) The distance between the best edge and the second best edge (on the opposite side of the best edge) is later than the snapport size in the axis There was a problem in our implementation. For example, in below diagram, D is the original destination of the given scroll operation, 1 is the best edge, 2 is the second best in our original implementation and 3 is of of the other snap points. In this case if there's an element larger than the distance between 1 and 3 and if the snapport size is larger than the distance between 1 and 2 but smaller than the distance between 1 and 3, we should apply `snap-overflow`. 2 1 D 3 |-|---|-----| There are a couple of test cases in wpt for this condition, for example there's one of them in oveflowing-snap-area.html [2]. That test case have been passed incorrectly due to our incorrect `snap-scope` implementation which will be fixed a subsequent change in this commit series. [1] https://drafts.csswg.org/css-scroll-snap-1/#snap-overflow [2] https://searchfox.org/mozilla-central/rev/13d69189a8abfc5064fe44944550b9b6eb4544f5/testing/web-platform/tests/css/css-scroll-snap/overflowing-snap-areas.html#131-137 Differential Revision: https://phabricator.services.mozilla.com/D144531
2022-05-10 11:51:37 +03:00
// 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);
mHorizontalEdgeFound = false;
mVerticalEdgeFound = false;
}
nsPoint CalcSnapPoints::GetBestEdge() const {
return nsPoint(mVerticalEdgeFound ? mBestEdge.x : mStartPos.x,
mHorizontalEdgeFound ? mBestEdge.y : mStartPos.y);
}
void CalcSnapPoints::AddHorizontalEdge(nscoord aEdge) {
AddEdge(aEdge, mDestination.y, mStartPos.y, mScrollingDirection.y,
&mBestEdge.y, &mSecondBestEdge.y, &mHorizontalEdgeFound);
}
void CalcSnapPoints::AddVerticalEdge(nscoord aEdge) {
AddEdge(aEdge, mDestination.x, mStartPos.x, mScrollingDirection.x,
&mBestEdge.x, &mSecondBestEdge.x, &mVerticalEdgeFound);
}
void CalcSnapPoints::AddEdge(nscoord aEdge, nscoord aDestination,
nscoord aStartPos, nscoord aScrollingDirection,
nscoord* aBestEdge, nscoord* 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) {
// 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;
}
}
if (!*aEdgeFound) {
*aBestEdge = aEdge;
*aEdgeFound = true;
return;
}
Bug 1766192 - Choose the second best edge on the opposite side of the best edge. r=botond The second best edge is used for `snap-overflow` feature [1]. The `snap-overflow` is applied when the following conditions are met. 1) A scroll snap target element is larger than the snapport (e.g. scrollport) in an axis 2) The distance between the best edge and the second best edge (on the opposite side of the best edge) is later than the snapport size in the axis There was a problem in our implementation. For example, in below diagram, D is the original destination of the given scroll operation, 1 is the best edge, 2 is the second best in our original implementation and 3 is of of the other snap points. In this case if there's an element larger than the distance between 1 and 3 and if the snapport size is larger than the distance between 1 and 2 but smaller than the distance between 1 and 3, we should apply `snap-overflow`. 2 1 D 3 |-|---|-----| There are a couple of test cases in wpt for this condition, for example there's one of them in oveflowing-snap-area.html [2]. That test case have been passed incorrectly due to our incorrect `snap-scope` implementation which will be fixed a subsequent change in this commit series. [1] https://drafts.csswg.org/css-scroll-snap-1/#snap-overflow [2] https://searchfox.org/mozilla-central/rev/13d69189a8abfc5064fe44944550b9b6eb4544f5/testing/web-platform/tests/css/css-scroll-snap/overflowing-snap-areas.html#131-137 Differential Revision: https://phabricator.services.mozilla.com/D144531
2022-05-10 11:51:37 +03:00
const bool isOnOppositeSide =
((aEdge - aDestination) > 0) != ((*aBestEdge - aDestination) > 0);
// 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.
auto updateBestEdges = [&](bool aIsCloserThanBest, bool aIsCloserThanSecond) {
if (aIsCloserThanBest) {
Bug 1766192 - Choose the second best edge on the opposite side of the best edge. r=botond The second best edge is used for `snap-overflow` feature [1]. The `snap-overflow` is applied when the following conditions are met. 1) A scroll snap target element is larger than the snapport (e.g. scrollport) in an axis 2) The distance between the best edge and the second best edge (on the opposite side of the best edge) is later than the snapport size in the axis There was a problem in our implementation. For example, in below diagram, D is the original destination of the given scroll operation, 1 is the best edge, 2 is the second best in our original implementation and 3 is of of the other snap points. In this case if there's an element larger than the distance between 1 and 3 and if the snapport size is larger than the distance between 1 and 2 but smaller than the distance between 1 and 3, we should apply `snap-overflow`. 2 1 D 3 |-|---|-----| There are a couple of test cases in wpt for this condition, for example there's one of them in oveflowing-snap-area.html [2]. That test case have been passed incorrectly due to our incorrect `snap-scope` implementation which will be fixed a subsequent change in this commit series. [1] https://drafts.csswg.org/css-scroll-snap-1/#snap-overflow [2] https://searchfox.org/mozilla-central/rev/13d69189a8abfc5064fe44944550b9b6eb4544f5/testing/web-platform/tests/css/css-scroll-snap/overflowing-snap-areas.html#131-137 Differential Revision: https://phabricator.services.mozilla.com/D144531
2022-05-10 11:51:37 +03:00
// 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) {
*aSecondBestEdge = *aBestEdge;
}
*aBestEdge = aEdge;
} else if (aIsCloserThanSecond) {
Bug 1766192 - Choose the second best edge on the opposite side of the best edge. r=botond The second best edge is used for `snap-overflow` feature [1]. The `snap-overflow` is applied when the following conditions are met. 1) A scroll snap target element is larger than the snapport (e.g. scrollport) in an axis 2) The distance between the best edge and the second best edge (on the opposite side of the best edge) is later than the snapport size in the axis There was a problem in our implementation. For example, in below diagram, D is the original destination of the given scroll operation, 1 is the best edge, 2 is the second best in our original implementation and 3 is of of the other snap points. In this case if there's an element larger than the distance between 1 and 3 and if the snapport size is larger than the distance between 1 and 2 but smaller than the distance between 1 and 3, we should apply `snap-overflow`. 2 1 D 3 |-|---|-----| There are a couple of test cases in wpt for this condition, for example there's one of them in oveflowing-snap-area.html [2]. That test case have been passed incorrectly due to our incorrect `snap-scope` implementation which will be fixed a subsequent change in this commit series. [1] https://drafts.csswg.org/css-scroll-snap-1/#snap-overflow [2] https://searchfox.org/mozilla-central/rev/13d69189a8abfc5064fe44944550b9b6eb4544f5/testing/web-platform/tests/css/css-scroll-snap/overflowing-snap-areas.html#131-137 Differential Revision: https://phabricator.services.mozilla.com/D144531
2022-05-10 11:51:37 +03:00
if (isOnOppositeSide) {
*aSecondBestEdge = aEdge;
}
}
};
if (mUnit == ScrollUnit::DEVICE_PIXELS || mUnit == ScrollUnit::LINES ||
mUnit == ScrollUnit::WHOLE) {
nscoord distance = std::abs(aEdge - aDestination);
Bug 1766192 - Choose the second best edge on the opposite side of the best edge. r=botond The second best edge is used for `snap-overflow` feature [1]. The `snap-overflow` is applied when the following conditions are met. 1) A scroll snap target element is larger than the snapport (e.g. scrollport) in an axis 2) The distance between the best edge and the second best edge (on the opposite side of the best edge) is later than the snapport size in the axis There was a problem in our implementation. For example, in below diagram, D is the original destination of the given scroll operation, 1 is the best edge, 2 is the second best in our original implementation and 3 is of of the other snap points. In this case if there's an element larger than the distance between 1 and 3 and if the snapport size is larger than the distance between 1 and 2 but smaller than the distance between 1 and 3, we should apply `snap-overflow`. 2 1 D 3 |-|---|-----| There are a couple of test cases in wpt for this condition, for example there's one of them in oveflowing-snap-area.html [2]. That test case have been passed incorrectly due to our incorrect `snap-scope` implementation which will be fixed a subsequent change in this commit series. [1] https://drafts.csswg.org/css-scroll-snap-1/#snap-overflow [2] https://searchfox.org/mozilla-central/rev/13d69189a8abfc5064fe44944550b9b6eb4544f5/testing/web-platform/tests/css/css-scroll-snap/overflowing-snap-areas.html#131-137 Differential Revision: https://phabricator.services.mozilla.com/D144531
2022-05-10 11:51:37 +03:00
updateBestEdges(
distance < std::abs(*aBestEdge - aDestination),
distance < std::abs(NSCoordSaturatingSubtract(
*aSecondBestEdge, aDestination, nscoord_MAX)));
} else if (mUnit == ScrollUnit::PAGES) {
// distance to the edge from the scrolling destination in the direction of
// scrolling
nscoord overshoot = (aEdge - aDestination) * aScrollingDirection;
// distance to the current best edge from the scrolling destination in the
// direction of scrolling
nscoord curOvershoot = (*aBestEdge - aDestination) * aScrollingDirection;
nscoord secondOvershoot =
Bug 1766192 - Choose the second best edge on the opposite side of the best edge. r=botond The second best edge is used for `snap-overflow` feature [1]. The `snap-overflow` is applied when the following conditions are met. 1) A scroll snap target element is larger than the snapport (e.g. scrollport) in an axis 2) The distance between the best edge and the second best edge (on the opposite side of the best edge) is later than the snapport size in the axis There was a problem in our implementation. For example, in below diagram, D is the original destination of the given scroll operation, 1 is the best edge, 2 is the second best in our original implementation and 3 is of of the other snap points. In this case if there's an element larger than the distance between 1 and 3 and if the snapport size is larger than the distance between 1 and 2 but smaller than the distance between 1 and 3, we should apply `snap-overflow`. 2 1 D 3 |-|---|-----| There are a couple of test cases in wpt for this condition, for example there's one of them in oveflowing-snap-area.html [2]. That test case have been passed incorrectly due to our incorrect `snap-scope` implementation which will be fixed a subsequent change in this commit series. [1] https://drafts.csswg.org/css-scroll-snap-1/#snap-overflow [2] https://searchfox.org/mozilla-central/rev/13d69189a8abfc5064fe44944550b9b6eb4544f5/testing/web-platform/tests/css/css-scroll-snap/overflowing-snap-areas.html#131-137 Differential Revision: https://phabricator.services.mozilla.com/D144531
2022-05-10 11:51:37 +03:00
NSCoordSaturatingSubtract(*aSecondBestEdge, aDestination, nscoord_MAX) *
aScrollingDirection;
// edges between the current position and the scrolling destination are
// favoured to preserve context
if (overshoot < 0) {
updateBestEdges(overshoot > curOvershoot || curOvershoot >= 0,
overshoot > secondOvershoot || secondOvershoot >= 0);
}
// if there are no edges between the current position and the scrolling
// destination the closest edge beyond the destination is used
if (overshoot > 0) {
updateBestEdges(overshoot < curOvershoot, overshoot < secondOvershoot);
}
} else {
NS_ERROR("Invalid scroll mode");
return;
}
}
static void ProcessSnapPositions(CalcSnapPoints& aCalcSnapPoints,
const ScrollSnapInfo& aSnapInfo) {
for (const auto& target : aSnapInfo.mSnapTargets) {
nsPoint snapPoint(target.mSnapPositionX ? *target.mSnapPositionX
: aCalcSnapPoints.Destination().x,
target.mSnapPositionY ? *target.mSnapPositionY
: aCalcSnapPoints.Destination().y);
nsRect snappedPort = nsRect(snapPoint, aSnapInfo.mSnapportSize);
// Ignore snap points if snapping to the point would leave the snap area
// outside of the snapport.
// https://drafts.csswg.org/css-scroll-snap-1/#snap-scope
if (!snappedPort.Intersects(target.mSnapArea)) {
continue;
}
if (target.mSnapPositionX) {
aCalcSnapPoints.AddVerticalEdge(*target.mSnapPositionX);
}
if (target.mSnapPositionY) {
aCalcSnapPoints.AddHorizontalEdge(*target.mSnapPositionY);
}
}
}
Maybe<nsPoint> ScrollSnapUtils::GetSnapPointForDestination(
const ScrollSnapInfo& aSnapInfo, ScrollUnit aUnit,
ScrollSnapFlags aSnapFlags, const nsRect& aScrollRange,
const nsPoint& aStartPos, const nsPoint& aDestination) {
if (aSnapInfo.mScrollSnapStrictnessY == StyleScrollSnapStrictness::None &&
aSnapInfo.mScrollSnapStrictnessX == StyleScrollSnapStrictness::None) {
return Nothing();
}
if (!aSnapInfo.HasSnapPositions()) {
return Nothing();
}
CalcSnapPoints calcSnapPoints(aUnit, aSnapFlags, aDestination, aStartPos);
ProcessSnapPositions(calcSnapPoints, aSnapInfo);
// If the distance between the first and the second candidate snap points
// is larger than the snapport size and the snapport is covered by larger
// elements, any points inside the covering area should be valid snap
// points.
// https://drafts.csswg.org/css-scroll-snap-1/#snap-overflow
// NOTE: |aDestination| sometimes points outside of the scroll range, e.g.
// by the APZC fling, so for the overflow checks we need to clamp it.
nsPoint clampedDestination = aScrollRange.ClampPoint(aDestination);
for (auto range : aSnapInfo.mXRangeWiderThanSnapport) {
if (range.IsValid(clampedDestination.x, aSnapInfo.mSnapportSize.width) &&
calcSnapPoints.XDistanceBetweenBestAndSecondEdge() >
aSnapInfo.mSnapportSize.width) {
calcSnapPoints.AddVerticalEdge(clampedDestination.x);
break;
}
}
for (auto range : aSnapInfo.mYRangeWiderThanSnapport) {
if (range.IsValid(clampedDestination.y, aSnapInfo.mSnapportSize.height) &&
calcSnapPoints.YDistanceBetweenBestAndSecondEdge() >
aSnapInfo.mSnapportSize.height) {
calcSnapPoints.AddHorizontalEdge(clampedDestination.y);
break;
}
}
bool snapped = false;
nsPoint finalPos = calcSnapPoints.GetBestEdge();
nscoord proximityThreshold =
StaticPrefs::layout_css_scroll_snap_proximity_threshold();
proximityThreshold = nsPresContext::CSSPixelsToAppUnits(proximityThreshold);
if (aSnapInfo.mScrollSnapStrictnessY ==
StyleScrollSnapStrictness::Proximity &&
std::abs(aDestination.y - finalPos.y) > proximityThreshold) {
finalPos.y = aDestination.y;
} else {
snapped = true;
}
if (aSnapInfo.mScrollSnapStrictnessX ==
StyleScrollSnapStrictness::Proximity &&
std::abs(aDestination.x - finalPos.x) > proximityThreshold) {
finalPos.x = aDestination.x;
} else {
snapped = true;
}
return snapped ? Some(finalPos) : Nothing();
}
} // namespace mozilla