Since https://github.com/w3c/csswg-drafts/issues/6863, if the scroll destination is located within the large snap area, the snapport no longer needs to overlap with the large area. So for example, the snapport size is 100px, and a larger snap area is located at [0px, 350px], and a small snap area is located at [350px, 400px] and both areas scroll-snap-align is `start`, the current scroll offset is at the bottom of the scroll range, 300px. In this case `scrollBy(-50px)` snaps to 0px in the previous implementation, now with this change it snaps to 250px. destination = 250px (scrollBy(-50px) from current offset 300px) v 0 100 250 350 400 | | | | | [ large area: align start ][sml] old: strict start-align jumps the snapport back to 0px <--------> new: nearest valid offset keeps the snapport at 250px <--------> (100px snapport still fully inside the large area) Because the large area (350px) is bigger than the snapport (100px), every offset in [0px, 250px] is a valid `start` snap (250 = 350 - 100). The destination 250px already lands in that range, so there is no reason to jump all the way back to 0px. Differential Revision: https://phabricator.services.mozilla.com/D305781
1006 lines
42 KiB
C++
1006 lines
42 KiB
C++
/* 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/ScrollContainerFrame.h"
|
|
#include "mozilla/ScrollSnapInfo.h"
|
|
#include "mozilla/ScrollSnapTargetId.h"
|
|
#include "mozilla/ServoStyleConsts.h"
|
|
#include "mozilla/StaticPrefs_layout.h"
|
|
#include "mozilla/dom/Document.h"
|
|
#include "nsContentUtils.h"
|
|
#include "nsIFrame.h"
|
|
#include "nsLayoutUtils.h"
|
|
#include "nsPresContext.h"
|
|
#include "nsString.h"
|
|
#include "nsTArray.h"
|
|
|
|
mozilla::LazyLogModule sApzScrollSnapLog("apz.scrollsnap");
|
|
#define SCROLL_SNAP_LOG(...) \
|
|
MOZ_LOG(sApzScrollSnapLog, LogLevel::Debug, (__VA_ARGS__))
|
|
|
|
namespace mozilla {
|
|
|
|
/**
|
|
* Keeps track of the current best edge to snap to. The criteria for
|
|
* adding an edge depends on the scrolling unit.
|
|
*/
|
|
class CalcSnapPoints final {
|
|
using SnapTarget = ScrollSnapInfo::SnapTarget;
|
|
|
|
public:
|
|
CalcSnapPoints(ScrollUnit aUnit, ScrollSnapFlags aSnapFlags,
|
|
const nsPoint& aDestination, const nsPoint& aStartPos,
|
|
const StyleScrollSnapStrictness& aXStrictness,
|
|
const StyleScrollSnapStrictness& aYStrictness);
|
|
struct SnapPosition : public SnapTarget {
|
|
SnapPosition(const SnapTarget& aSnapTarget, nscoord aPosition,
|
|
nscoord aDistanceOnOtherAxis)
|
|
: SnapTarget(aSnapTarget),
|
|
mPosition(aPosition),
|
|
mDistanceOnOtherAxis(aDistanceOnOtherAxis) {}
|
|
|
|
nscoord mPosition;
|
|
// The distance from the scroll destination to this snap position on the
|
|
// other axis. This value is used if there are multiple SnapPositions on
|
|
// this axis, but the positions on the other axis are different.
|
|
nscoord mDistanceOnOtherAxis;
|
|
};
|
|
|
|
void AddHorizontalEdge(const SnapTarget& aTarget);
|
|
void AddVerticalEdge(const SnapTarget& aTarget);
|
|
|
|
struct CandidateTracker {
|
|
// keeps track of the position of the current second best edge on the
|
|
// opposite side of the best edge on this axis.
|
|
// 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.
|
|
nscoord mSecondBestEdge = nscoord_MAX;
|
|
|
|
// Assuming in most cases there's no multiple coincide snap points.
|
|
AutoTArray<ScrollSnapTargetId, 1> mTargetIds;
|
|
// keeps track of the positions of the current best edge on this axis.
|
|
// NOTE: Each SnapPosition.mPosition points the same snap position on this
|
|
// axis but other member variables of SnapPosition may have different
|
|
// values.
|
|
AutoTArray<SnapPosition, 1> mBestEdges;
|
|
bool EdgeFound() const { return !mBestEdges.IsEmpty(); }
|
|
};
|
|
void AddEdge(const SnapPosition& aEdge, nscoord aDestination,
|
|
nscoord aStartPos, nscoord aScrollingDirection,
|
|
CandidateTracker* aCandidateTracker);
|
|
SnapDestination GetBestEdge(const nsSize& aSnapportSize) const;
|
|
nsPoint GetDefaultSnapPoint() const;
|
|
nscoord XDistanceBetweenBestAndSecondEdge() const {
|
|
return std::abs(NSCoordSaturatingSubtract(
|
|
mTrackerOnX.mSecondBestEdge,
|
|
mTrackerOnX.EdgeFound() ? mTrackerOnX.mBestEdges[0].mPosition
|
|
: mDestination.x,
|
|
nscoord_MAX));
|
|
}
|
|
nscoord YDistanceBetweenBestAndSecondEdge() const {
|
|
return std::abs(NSCoordSaturatingSubtract(
|
|
mTrackerOnY.mSecondBestEdge,
|
|
mTrackerOnY.EdgeFound() ? mTrackerOnY.mBestEdges[0].mPosition
|
|
: mDestination.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
|
|
StyleScrollSnapStrictness mStrictnessX;
|
|
StyleScrollSnapStrictness mStrictnessY;
|
|
CandidateTracker mTrackerOnX;
|
|
CandidateTracker mTrackerOnY;
|
|
};
|
|
|
|
CalcSnapPoints::CalcSnapPoints(ScrollUnit aUnit, ScrollSnapFlags aSnapFlags,
|
|
const nsPoint& aDestination,
|
|
const nsPoint& aStartPos,
|
|
const StyleScrollSnapStrictness& aXStrictness,
|
|
const StyleScrollSnapStrictness& aYStrictness)
|
|
: mUnit(aUnit),
|
|
mSnapFlags(aSnapFlags),
|
|
mDestination(aDestination),
|
|
mStartPos(aStartPos),
|
|
mStrictnessX(aXStrictness),
|
|
mStrictnessY(aYStrictness) {
|
|
MOZ_ASSERT(aSnapFlags != ScrollSnapFlags::Disabled);
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
nsPoint CalcSnapPoints::GetDefaultSnapPoint() const {
|
|
MOZ_ASSERT(mSnapFlags != ScrollSnapFlags::Disabled);
|
|
nsPoint defaultPoint = mDestination;
|
|
if ((mSnapFlags & ScrollSnapFlags::IntendedDirection) ==
|
|
ScrollSnapFlags::IntendedDirection) {
|
|
if (mStrictnessX != StyleScrollSnapStrictness::Proximity) {
|
|
defaultPoint.x = mStartPos.x;
|
|
}
|
|
if (mStrictnessY != StyleScrollSnapStrictness::Proximity) {
|
|
defaultPoint.y = mStartPos.y;
|
|
}
|
|
}
|
|
return defaultPoint;
|
|
}
|
|
|
|
SnapDestination CalcSnapPoints::GetBestEdge(const nsSize& aSnapportSize) const {
|
|
if (mTrackerOnX.EdgeFound() && mTrackerOnY.EdgeFound()) {
|
|
nsPoint bestCandidate(mTrackerOnX.mBestEdges[0].mPosition,
|
|
mTrackerOnY.mBestEdges[0].mPosition);
|
|
nsRect snappedPort = nsRect(bestCandidate, aSnapportSize);
|
|
|
|
// If we've found the candidates on both axes, it's possible some of
|
|
// candidates will be outside of the snapport if we snap to the point
|
|
// (mTrackerOnX.mBestEdges[0].mPosition,
|
|
// mTrackerOnY.mBestEdges[0].mPosition). So we need to get the intersection
|
|
// of the snap area of each snap target element on each axis and the
|
|
// snapport to tell whether it's outside of the snapport or not.
|
|
//
|
|
// Also if at least either one of the elements will be outside of the
|
|
// snapport if we snap to (mTrackerOnX.mBestEdges[0].mPosition,
|
|
// mTrackerOnY.mBestEdges[0].mPosition). We need to choose one of
|
|
// combinations of the candidates which is closest to the destination.
|
|
//
|
|
// So here we iterate over mTrackerOnX and mTrackerOnY just once
|
|
// respectively for both purposes to avoid iterating over them again and
|
|
// again.
|
|
//
|
|
// NOTE: Ideally we have to iterate over every possible combinations of
|
|
// (mTrackerOnX.mBestEdges[i].mSnapPoint.mY,
|
|
// mTrackerOnY.mBestEdges[j].mSnapPoint.mX) and tell whether the given
|
|
// combination will be visible in the snapport or not (maybe we should
|
|
// choose the one that the visible area, i.e., the intersection area of
|
|
// the snap target elements and the snapport, is the largest one rather than
|
|
// the closest one?). But it will be inefficient, so here we will not
|
|
// iterate all the combinations, we just iterate all the snap target
|
|
// elements in each axis respectively.
|
|
|
|
AutoTArray<ScrollSnapTargetId, 1> visibleTargetIdsOnX;
|
|
nscoord minimumDistanceOnY = nscoord_MAX;
|
|
size_t minimumXIndex = 0;
|
|
AutoTArray<ScrollSnapTargetId, 1> minimumDistanceTargetIdsOnX;
|
|
for (size_t i = 0; i < mTrackerOnX.mBestEdges.Length(); i++) {
|
|
const auto& targetX = mTrackerOnX.mBestEdges[i];
|
|
if (targetX.mSnapArea.Intersects(snappedPort)) {
|
|
visibleTargetIdsOnX.AppendElement(targetX.mTargetId);
|
|
}
|
|
|
|
if (targetX.mDistanceOnOtherAxis < minimumDistanceOnY) {
|
|
minimumDistanceOnY = targetX.mDistanceOnOtherAxis;
|
|
minimumXIndex = i;
|
|
minimumDistanceTargetIdsOnX =
|
|
AutoTArray<ScrollSnapTargetId, 1>{targetX.mTargetId};
|
|
} else if (minimumDistanceOnY != nscoord_MAX &&
|
|
targetX.mDistanceOnOtherAxis == minimumDistanceOnY) {
|
|
minimumDistanceTargetIdsOnX.AppendElement(targetX.mTargetId);
|
|
}
|
|
}
|
|
|
|
AutoTArray<ScrollSnapTargetId, 1> visibleTargetIdsOnY;
|
|
nscoord minimumDistanceOnX = nscoord_MAX;
|
|
size_t minimumYIndex = 0;
|
|
AutoTArray<ScrollSnapTargetId, 1> minimumDistanceTargetIdsOnY;
|
|
for (size_t i = 0; i < mTrackerOnY.mBestEdges.Length(); i++) {
|
|
const auto& targetY = mTrackerOnY.mBestEdges[i];
|
|
if (targetY.mSnapArea.Intersects(snappedPort)) {
|
|
visibleTargetIdsOnY.AppendElement(targetY.mTargetId);
|
|
}
|
|
|
|
if (targetY.mDistanceOnOtherAxis < minimumDistanceOnX) {
|
|
minimumDistanceOnX = targetY.mDistanceOnOtherAxis;
|
|
minimumYIndex = i;
|
|
minimumDistanceTargetIdsOnY =
|
|
AutoTArray<ScrollSnapTargetId, 1>{targetY.mTargetId};
|
|
} else if (minimumDistanceOnX != nscoord_MAX &&
|
|
targetY.mDistanceOnOtherAxis == minimumDistanceOnX) {
|
|
minimumDistanceTargetIdsOnY.AppendElement(targetY.mTargetId);
|
|
}
|
|
}
|
|
|
|
// If we have the target ids on both axes, it means the target elements
|
|
// (ids) specifying the best edge on X axis and the target elements
|
|
// specifying the best edge on Y axis are visible if we snap to the best
|
|
// edge. Thus they are valid snap positions.
|
|
if (!visibleTargetIdsOnX.IsEmpty() && !visibleTargetIdsOnY.IsEmpty()) {
|
|
return SnapDestination{
|
|
bestCandidate,
|
|
ScrollSnapTargetIds{visibleTargetIdsOnX, visibleTargetIdsOnY}};
|
|
}
|
|
|
|
// Now we've already known that snapping to
|
|
// (mTrackerOnX.mBestEdges[0].mPosition,
|
|
// mTrackerOnY.mBestEdges[0].mPosition) will make all candidates of
|
|
// mTrackerX or mTrackerY (or both) outside of the snapport. We need to
|
|
// choose another combination where candidates of both mTrackerX/Y are
|
|
// inside the snapport.
|
|
|
|
// There are three possibilities;
|
|
// 1) There's no candidate on X axis in mTrackerOnY (that means
|
|
// each candidate's scroll-snap-align is `none` on X axis), but there's
|
|
// any candidate in mTrackerOnX, the closest candidates of mTrackerOnX
|
|
// should be used.
|
|
// 2) There's no candidate on Y axis in mTrackerOnX (that means
|
|
// each candidate's scroll-snap-align is `none` on Y axis), but there's
|
|
// any candidate in mTrackerOnY, the closest candidates of mTrackerOnY
|
|
// should be used.
|
|
// 3) There are candidates on both axes. Choosing a combination such as
|
|
// (mTrackerOnX.mBestEdges[i].mSnapPoint.mX,
|
|
// mTrackerOnY.mBestEdges[i].mSnapPoint.mY)
|
|
// would require us to iterate over the candidates again if the
|
|
// combination position is outside the snapport, which we don't want to
|
|
// do. Instead, we choose either one of the axis' candidates.
|
|
if ((minimumDistanceOnX == nscoord_MAX) &&
|
|
minimumDistanceOnY != nscoord_MAX) {
|
|
bestCandidate.y = *mTrackerOnX.mBestEdges[minimumXIndex].mSnapPoint.mY;
|
|
return SnapDestination{bestCandidate,
|
|
ScrollSnapTargetIds{minimumDistanceTargetIdsOnX,
|
|
minimumDistanceTargetIdsOnX}};
|
|
}
|
|
|
|
if (minimumDistanceOnX != nscoord_MAX &&
|
|
minimumDistanceOnY == nscoord_MAX) {
|
|
bestCandidate.x = *mTrackerOnY.mBestEdges[minimumYIndex].mSnapPoint.mX;
|
|
return SnapDestination{bestCandidate,
|
|
ScrollSnapTargetIds{minimumDistanceTargetIdsOnY,
|
|
minimumDistanceTargetIdsOnY}};
|
|
}
|
|
|
|
if (minimumDistanceOnX != nscoord_MAX &&
|
|
minimumDistanceOnY != nscoord_MAX) {
|
|
// If we've found candidates on both axes, choose the closest point either
|
|
// on X axis or Y axis from the scroll destination. I.e. choose
|
|
// `minimumXIndex` one or `minimumYIndex` one to make at least one of
|
|
// snap target elements visible inside the snapport.
|
|
//
|
|
// For example,
|
|
// [bestCandidate.x, mTrackerOnX.mBestEdges[minimumXIndex].mSnapPoint.mY]
|
|
// is a candidate generated from a single element, thus snapping to the
|
|
// point would definitely make the element visible inside the snapport.
|
|
if (hypotf(NSCoordToFloat(mDestination.x -
|
|
mTrackerOnX.mBestEdges[0].mPosition),
|
|
NSCoordToFloat(minimumDistanceOnY)) <
|
|
hypotf(NSCoordToFloat(minimumDistanceOnX),
|
|
NSCoordToFloat(mDestination.y -
|
|
mTrackerOnY.mBestEdges[0].mPosition))) {
|
|
bestCandidate.y = *mTrackerOnX.mBestEdges[minimumXIndex].mSnapPoint.mY;
|
|
} else {
|
|
bestCandidate.x = *mTrackerOnY.mBestEdges[minimumYIndex].mSnapPoint.mX;
|
|
}
|
|
return SnapDestination{bestCandidate,
|
|
ScrollSnapTargetIds{minimumDistanceTargetIdsOnX,
|
|
minimumDistanceTargetIdsOnY}};
|
|
}
|
|
MOZ_ASSERT_UNREACHABLE("There's at least one candidate on either axis");
|
|
// `minimumDistanceOnX == nscoord_MAX && minimumDistanceOnY == nscoord_MAX`
|
|
// should not happen but we fall back for safety.
|
|
}
|
|
|
|
nsPoint defaultPoint = GetDefaultSnapPoint();
|
|
return SnapDestination{
|
|
nsPoint(mTrackerOnX.EdgeFound()
|
|
? mTrackerOnX.mBestEdges[0].mPosition
|
|
// In the case of IntendedEndPosition (i.e. the destination
|
|
// point is explicitely specied, e.g. scrollTo) use the
|
|
// destination point if we didn't find any candidates.
|
|
: defaultPoint.x,
|
|
mTrackerOnY.EdgeFound()
|
|
? mTrackerOnY.mBestEdges[0].mPosition
|
|
// Same as above X axis case, use the destination point if we
|
|
// didn't find any candidates.
|
|
: defaultPoint.y),
|
|
ScrollSnapTargetIds{mTrackerOnX.mTargetIds, mTrackerOnY.mTargetIds}};
|
|
}
|
|
|
|
void CalcSnapPoints::AddHorizontalEdge(const SnapTarget& aTarget) {
|
|
MOZ_ASSERT(aTarget.mSnapPoint.mY);
|
|
AddEdge(SnapPosition{aTarget, *aTarget.mSnapPoint.mY,
|
|
aTarget.mSnapPoint.mX
|
|
? std::abs(mDestination.x - *aTarget.mSnapPoint.mX)
|
|
: nscoord_MAX},
|
|
mDestination.y, mStartPos.y, mScrollingDirection.y, &mTrackerOnY);
|
|
}
|
|
|
|
void CalcSnapPoints::AddVerticalEdge(const SnapTarget& aTarget) {
|
|
MOZ_ASSERT(aTarget.mSnapPoint.mX);
|
|
AddEdge(SnapPosition{aTarget, *aTarget.mSnapPoint.mX,
|
|
aTarget.mSnapPoint.mY
|
|
? std::abs(mDestination.y - *aTarget.mSnapPoint.mY)
|
|
: nscoord_MAX},
|
|
mDestination.x, mStartPos.x, mScrollingDirection.x, &mTrackerOnX);
|
|
}
|
|
|
|
void CalcSnapPoints::AddEdge(const SnapPosition& aEdge, nscoord aDestination,
|
|
nscoord aStartPos, nscoord aScrollingDirection,
|
|
CandidateTracker* aCandidateTracker) {
|
|
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.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;
|
|
}
|
|
}
|
|
|
|
if (!aCandidateTracker->EdgeFound()) {
|
|
aCandidateTracker->mBestEdges = AutoTArray<SnapPosition, 1>{aEdge};
|
|
aCandidateTracker->mTargetIds =
|
|
AutoTArray<ScrollSnapTargetId, 1>{aEdge.mTargetId};
|
|
return;
|
|
}
|
|
|
|
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) !=
|
|
((aCandidateTracker->mBestEdges[0].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) {
|
|
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
|
|
// |------------|---------|------|
|
|
aCandidateTracker->mSecondBestEdge = aEdge.mPosition;
|
|
} 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.
|
|
aCandidateTracker->mSecondBestEdge =
|
|
aCandidateTracker->mBestEdges[0].mPosition;
|
|
}
|
|
aCandidateTracker->mBestEdges = AutoTArray<SnapPosition, 1>{aEdge};
|
|
aCandidateTracker->mTargetIds =
|
|
AutoTArray<ScrollSnapTargetId, 1>{aEdge.mTargetId};
|
|
} else {
|
|
if (aEdge.mPosition == aCandidateTracker->mBestEdges[0].mPosition) {
|
|
aCandidateTracker->mTargetIds.AppendElement(aEdge.mTargetId);
|
|
aCandidateTracker->mBestEdges.AppendElement(aEdge);
|
|
}
|
|
if (aIsCloserThanSecond && isOnOppositeSide) {
|
|
aCandidateTracker->mSecondBestEdge = aEdge.mPosition;
|
|
}
|
|
}
|
|
};
|
|
|
|
bool isCandidateOfBest = false;
|
|
bool isCandidateOfSecondBest = false;
|
|
switch (mUnit) {
|
|
case ScrollUnit::DEVICE_PIXELS:
|
|
case ScrollUnit::LINES:
|
|
case ScrollUnit::WHOLE: {
|
|
isCandidateOfBest =
|
|
std::abs(distanceFromDestination) <
|
|
std::abs(aCandidateTracker->mBestEdges[0].mPosition - aDestination);
|
|
isCandidateOfSecondBest =
|
|
std::abs(distanceFromDestination) <
|
|
std::abs(NSCoordSaturatingSubtract(aCandidateTracker->mSecondBestEdge,
|
|
aDestination, nscoord_MAX));
|
|
break;
|
|
}
|
|
case ScrollUnit::PAGES: {
|
|
// distance to the edge from the scrolling destination in the direction of
|
|
// scrolling
|
|
nscoord overshoot = distanceFromDestination * aScrollingDirection;
|
|
// distance to the current best edge from the scrolling destination in the
|
|
// direction of scrolling
|
|
nscoord curOvershoot =
|
|
(aCandidateTracker->mBestEdges[0].mPosition - aDestination) *
|
|
aScrollingDirection;
|
|
|
|
nscoord secondOvershoot =
|
|
NSCoordSaturatingSubtract(aCandidateTracker->mSecondBestEdge,
|
|
aDestination, nscoord_MAX) *
|
|
aScrollingDirection;
|
|
|
|
// edges between the current position and the scrolling destination are
|
|
// favoured to preserve context
|
|
if (overshoot < 0) {
|
|
isCandidateOfBest = overshoot > curOvershoot || curOvershoot >= 0;
|
|
isCandidateOfSecondBest =
|
|
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) {
|
|
isCandidateOfBest = overshoot < curOvershoot;
|
|
isCandidateOfSecondBest = overshoot < secondOvershoot;
|
|
}
|
|
}
|
|
}
|
|
|
|
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(aCandidateTracker->mBestEdges[0].mPosition - aStartPos);
|
|
} else if (isPreferredStopAlways(aCandidateTracker->mBestEdges[0])) {
|
|
// 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);
|
|
}
|
|
|
|
using SnapTarget = ScrollSnapInfo::SnapTarget;
|
|
|
|
static void ProcessSnapPositions(CalcSnapPoints& aCalcSnapPoints,
|
|
const ScrollSnapInfo& aSnapInfo) {
|
|
aSnapInfo.ForEachValidTargetFor(
|
|
aCalcSnapPoints.Destination(), [&](const SnapTarget& aTarget) -> bool {
|
|
if (aTarget.mSnapPoint.mX && aSnapInfo.mScrollSnapStrictnessX !=
|
|
StyleScrollSnapStrictness::None) {
|
|
aCalcSnapPoints.AddVerticalEdge(aTarget);
|
|
}
|
|
if (aTarget.mSnapPoint.mY && aSnapInfo.mScrollSnapStrictnessY !=
|
|
StyleScrollSnapStrictness::None) {
|
|
aCalcSnapPoints.AddHorizontalEdge(aTarget);
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
|
|
static void ProcessSnapOverflowForAxis(
|
|
CalcSnapPoints& aCalcSnapPoints, layers::ScrollDirection aScrollDirection,
|
|
nscoord aClampedDestination, nscoord aSnapportSize,
|
|
const nsTArray<ScrollSnapInfo::ScrollSnapRange>& aRanges) {
|
|
auto addEdge = [&](nscoord aSnapPoint, const nsRect& aSnapArea) {
|
|
if (aScrollDirection == layers::ScrollDirection::eHorizontal) {
|
|
aCalcSnapPoints.AddVerticalEdge(ScrollSnapInfo::SnapTarget{
|
|
Some(aSnapPoint), Nothing(), aSnapArea, StyleScrollSnapStop::Normal,
|
|
ScrollSnapTargetId::None});
|
|
} else {
|
|
aCalcSnapPoints.AddHorizontalEdge(ScrollSnapInfo::SnapTarget{
|
|
Nothing(), Some(aSnapPoint), aSnapArea, StyleScrollSnapStop::Normal,
|
|
ScrollSnapTargetId::None});
|
|
}
|
|
};
|
|
|
|
for (const auto& range : aRanges) {
|
|
if (range.IsValid(aClampedDestination, aSnapportSize)) {
|
|
addEdge(range.FindNearestSnapPoint(aClampedDestination, aSnapportSize),
|
|
range.mSnapArea);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void ProcessSnapOverflow(CalcSnapPoints& aCalcSnapPoints,
|
|
const ScrollSnapInfo& aSnapInfo,
|
|
const nsRect& aScrollRange,
|
|
const nsPoint& aDestination) {
|
|
// 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);
|
|
if (aCalcSnapPoints.XDistanceBetweenBestAndSecondEdge() >
|
|
aSnapInfo.mSnapportSize.width) {
|
|
ProcessSnapOverflowForAxis(
|
|
aCalcSnapPoints, layers::ScrollDirection::eHorizontal,
|
|
clampedDestination.x, aSnapInfo.mSnapportSize.width,
|
|
aSnapInfo.mXRangeWiderThanSnapport);
|
|
}
|
|
if (aCalcSnapPoints.YDistanceBetweenBestAndSecondEdge() >
|
|
aSnapInfo.mSnapportSize.height) {
|
|
ProcessSnapOverflowForAxis(
|
|
aCalcSnapPoints, layers::ScrollDirection::eVertical,
|
|
clampedDestination.y, aSnapInfo.mSnapportSize.height,
|
|
aSnapInfo.mYRangeWiderThanSnapport);
|
|
}
|
|
}
|
|
|
|
Maybe<SnapDestination> 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,
|
|
aSnapInfo.mScrollSnapStrictnessX,
|
|
aSnapInfo.mScrollSnapStrictnessY);
|
|
|
|
ProcessSnapPositions(calcSnapPoints, aSnapInfo);
|
|
ProcessSnapOverflow(calcSnapPoints, aSnapInfo, aScrollRange, aDestination);
|
|
|
|
bool snapped = false;
|
|
auto finalPos = calcSnapPoints.GetBestEdge(aSnapInfo.mSnapportSize);
|
|
|
|
// Check whether we will snap to the final position on the given axis or not,
|
|
// and if we will not, reset the final position to the original position so
|
|
// that even if we need to snap on an axis, but we don't need to on the other
|
|
// axis, the returned final position can be used as a valid destination.
|
|
auto checkSnapOnAxis = [&snapped](StyleScrollSnapStrictness aStrictness,
|
|
nscoord aDestination, nscoord aSnapportSize,
|
|
nscoord& aFinalPosition) {
|
|
// We used 0.3 proximity threshold which is what WebKit uses.
|
|
constexpr float proximityRatio = 0.3;
|
|
if (aStrictness == StyleScrollSnapStrictness::None ||
|
|
(aStrictness == StyleScrollSnapStrictness::Proximity &&
|
|
std::abs(aDestination - aFinalPosition) >
|
|
aSnapportSize * proximityRatio)) {
|
|
aFinalPosition = aDestination;
|
|
return;
|
|
}
|
|
snapped = true;
|
|
};
|
|
|
|
checkSnapOnAxis(aSnapInfo.mScrollSnapStrictnessY, aDestination.y,
|
|
aSnapInfo.mSnapportSize.height, finalPos.mPosition.y);
|
|
checkSnapOnAxis(aSnapInfo.mScrollSnapStrictnessX, aDestination.x,
|
|
aSnapInfo.mSnapportSize.width, finalPos.mPosition.x);
|
|
|
|
return snapped ? Some(finalPos) : Nothing();
|
|
}
|
|
|
|
ScrollSnapTargetId ScrollSnapUtils::GetTargetIdFor(const nsIFrame* aFrame) {
|
|
MOZ_ASSERT(aFrame && aFrame->GetContent());
|
|
return ScrollSnapTargetId{reinterpret_cast<uintptr_t>(aFrame->GetContent())};
|
|
}
|
|
|
|
static const nsIContent* ResolveSnapTargetToContent(
|
|
const ScrollSnapTargetId& aId) {
|
|
if (aId == ScrollSnapTargetId::None) {
|
|
return nullptr;
|
|
}
|
|
return reinterpret_cast<const nsIContent*>(aId);
|
|
}
|
|
|
|
static bool SnapTargetIsFlattenedTreeDescendantOf(
|
|
const ScrollSnapTargetId& aPossibleDescendant,
|
|
const ScrollSnapTargetId& aPossibleAncestor) {
|
|
MOZ_ASSERT(aPossibleAncestor != ScrollSnapTargetId::None &&
|
|
aPossibleDescendant != ScrollSnapTargetId::None);
|
|
return nsContentUtils::ContentIsFlattenedTreeDescendantOf(
|
|
ResolveSnapTargetToContent(aPossibleDescendant),
|
|
ResolveSnapTargetToContent(aPossibleAncestor));
|
|
}
|
|
|
|
static std::pair<Maybe<nscoord>, Maybe<nscoord>> GetCandidateInLastTargets(
|
|
const ScrollSnapInfo& aSnapInfo, const nsPoint& aCurrentPosition,
|
|
const UniquePtr<ScrollSnapTargetIds>& aLastSnapTargetIds,
|
|
const nsIContent* aFocusedContent, const nsIContent* aTargetContent,
|
|
const WritingMode aWM) {
|
|
auto GetTargetId = [](const nsIContent* aContent) -> ScrollSnapTargetId {
|
|
if (aContent && aContent->GetPrimaryFrame()) {
|
|
return ScrollSnapUtils::GetTargetIdFor(aContent->GetPrimaryFrame());
|
|
}
|
|
return ScrollSnapTargetId::None;
|
|
};
|
|
|
|
ScrollSnapTargetId targetIdForFocusedContent = GetTargetId(aFocusedContent);
|
|
ScrollSnapTargetId targetIdForTargetContent = GetTargetId(aTargetContent);
|
|
const bool isVertical = aWM.IsVertical();
|
|
|
|
// Note: Below algorithm doesn't care about cases where the last snap point
|
|
// was on an element larger than the snapport since it's not clear to us
|
|
// what we should do for now.
|
|
// https://github.com/w3c/csswg-drafts/issues/7438
|
|
|
|
// Build the inline and block candidate sets from the last snap targets, per
|
|
// https://drafts.csswg.org/css-scroll-snap-1/#multiple-aligned-snap-areas
|
|
AutoTArray<const ScrollSnapInfo::SnapTarget*, 2> inlineSet, blockSet;
|
|
const ScrollSnapInfo::SnapTarget* focusedTarget = nullptr;
|
|
const ScrollSnapInfo::SnapTarget* targetedTarget = nullptr;
|
|
|
|
aSnapInfo.ForEachValidTargetFor(
|
|
aCurrentPosition, [&](const SnapTarget& aTarget) -> bool {
|
|
if (aTarget.mSnapPoint.I(aWM) &&
|
|
aSnapInfo.StrictnessInline(aWM) !=
|
|
StyleScrollSnapStrictness::None &&
|
|
aLastSnapTargetIds->IdsOnInline(aWM).Contains(aTarget.mTargetId)) {
|
|
inlineSet.AppendElement(&aTarget);
|
|
}
|
|
if (aTarget.mSnapPoint.B(aWM) &&
|
|
aSnapInfo.StrictnessBlock(aWM) != StyleScrollSnapStrictness::None &&
|
|
aLastSnapTargetIds->IdsOnBlock(aWM).Contains(aTarget.mTargetId)) {
|
|
blockSet.AppendElement(&aTarget);
|
|
}
|
|
if (aLastSnapTargetIds->Contains(aTarget.mTargetId)) {
|
|
if (aTarget.mTargetId == targetIdForFocusedContent ||
|
|
(targetIdForFocusedContent != ScrollSnapTargetId::None &&
|
|
SnapTargetIsFlattenedTreeDescendantOf(targetIdForFocusedContent,
|
|
aTarget.mTargetId))) {
|
|
focusedTarget = &aTarget;
|
|
}
|
|
if (aTarget.mTargetId == targetIdForTargetContent) {
|
|
targetedTarget = &aTarget;
|
|
}
|
|
}
|
|
return true;
|
|
});
|
|
|
|
if (MOZ_LOG_TEST(sApzScrollSnapLog, LogLevel::Debug)) {
|
|
SCROLL_SNAP_LOG("All snap targets: %s",
|
|
ToString(aSnapInfo.mSnapTargets).c_str());
|
|
SCROLL_SNAP_LOG("Inline snap targets: %s", ToString(inlineSet).c_str());
|
|
SCROLL_SNAP_LOG("Block snap targets: %s", ToString(blockSet).c_str());
|
|
}
|
|
|
|
// Step 4.1: If the focused element is in a set, it's the only candidate.
|
|
if (focusedTarget) {
|
|
if (focusedTarget->mSnapPoint.I(aWM) &&
|
|
aSnapInfo.StrictnessInline(aWM) != StyleScrollSnapStrictness::None) {
|
|
inlineSet = {focusedTarget};
|
|
}
|
|
if (focusedTarget->mSnapPoint.B(aWM) &&
|
|
aSnapInfo.StrictnessBlock(aWM) != StyleScrollSnapStrictness::None) {
|
|
blockSet = {focusedTarget};
|
|
}
|
|
}
|
|
|
|
// Step 4.2: If no focused element was found but the :target element is
|
|
// in a set, it's the only candidate.
|
|
if (!focusedTarget && targetedTarget) {
|
|
if (targetedTarget->mSnapPoint.I(aWM) &&
|
|
aSnapInfo.StrictnessInline(aWM) != StyleScrollSnapStrictness::None) {
|
|
inlineSet = {targetedTarget};
|
|
}
|
|
if (targetedTarget->mSnapPoint.B(aWM) &&
|
|
aSnapInfo.StrictnessBlock(aWM) != StyleScrollSnapStrictness::None) {
|
|
blockSet = {targetedTarget};
|
|
}
|
|
}
|
|
|
|
// Step 4.3: For each box in a set, remove any box from the set that is an
|
|
// ancestor of that box.
|
|
auto removeAncestors =
|
|
[](AutoTArray<const ScrollSnapInfo::SnapTarget*, 2>& aSet) {
|
|
if (aSet.Length() <= 1) {
|
|
return;
|
|
}
|
|
AutoTArray<const ScrollSnapInfo::SnapTarget*, 2> result;
|
|
for (const auto* candidate : aSet) {
|
|
bool isAncestorOfAnotherInSet = false;
|
|
for (const auto* other : aSet) {
|
|
if (other == candidate) {
|
|
continue;
|
|
}
|
|
if (SnapTargetIsFlattenedTreeDescendantOf(other->mTargetId,
|
|
candidate->mTargetId)) {
|
|
isAncestorOfAnotherInSet = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!isAncestorOfAnotherInSet) {
|
|
result.AppendElement(candidate);
|
|
}
|
|
}
|
|
aSet = std::move(result);
|
|
};
|
|
removeAncestors(inlineSet);
|
|
removeAncestors(blockSet);
|
|
|
|
// Step 5: If the inline and block sets overlap (share at least one element),
|
|
// replace both with their intersection. If they are disjoint, the block axis
|
|
// set takes precedence and is used for both axes.
|
|
AutoTArray<const ScrollSnapInfo::SnapTarget*, 2> intersection;
|
|
for (const auto* inlineTarget : inlineSet) {
|
|
for (const auto* blockTarget : blockSet) {
|
|
if (inlineTarget->mTargetId == blockTarget->mTargetId) {
|
|
intersection.AppendElement(inlineTarget);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
const auto& effective = !intersection.IsEmpty() ? intersection
|
|
: !blockSet.IsEmpty() ? blockSet
|
|
: inlineSet;
|
|
|
|
// Select the first candidate from each set in tree order, using a
|
|
// cross-axis visibility check to prefer targets whose snap area remains
|
|
// visible at the combined snap position.
|
|
Maybe<nscoord> x, y;
|
|
|
|
const ScrollSnapInfo::SnapTarget* inlinePick{nullptr};
|
|
const ScrollSnapInfo::SnapTarget* blockPick{nullptr};
|
|
auto pickFromInline = [&]() {
|
|
Maybe<nscoord>& inlineCoord = isVertical ? y : x;
|
|
const Maybe<nscoord>& blockCoord = isVertical ? x : y;
|
|
for (const auto* target : effective) {
|
|
// When effective == blockSet, targets may not have an inline snap point.
|
|
const auto& sp = target->mSnapPoint.I(aWM);
|
|
if (!sp) {
|
|
continue;
|
|
}
|
|
if (!blockCoord || target->mSnapArea.Intersects(
|
|
nsRect(isVertical ? nsPoint(*blockCoord, *sp)
|
|
: nsPoint(*sp, *blockCoord),
|
|
aSnapInfo.mSnapportSize))) {
|
|
inlineCoord = sp;
|
|
inlinePick = target;
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
|
|
auto pickFromBlock = [&]() {
|
|
Maybe<nscoord>& blockCoord = isVertical ? x : y;
|
|
const Maybe<nscoord>& inlineCoord = isVertical ? y : x;
|
|
for (const auto* target : effective) {
|
|
// When effective == inlineSet, targets may not have a block snap point.
|
|
const auto& sp = target->mSnapPoint.B(aWM);
|
|
if (!sp) {
|
|
continue;
|
|
}
|
|
if (!inlineCoord || target->mSnapArea.Intersects(
|
|
nsRect(isVertical ? nsPoint(*sp, *inlineCoord)
|
|
: nsPoint(*inlineCoord, *sp),
|
|
aSnapInfo.mSnapportSize))) {
|
|
blockCoord = sp;
|
|
blockPick = target;
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
|
|
// Only pick snap positions for axes that have snapping enabled.
|
|
if (aSnapInfo.StrictnessInline(aWM) != StyleScrollSnapStrictness::None) {
|
|
pickFromInline();
|
|
if (inlinePick && MOZ_LOG_TEST(sApzScrollSnapLog, LogLevel::Debug)) {
|
|
SCROLL_SNAP_LOG("Inline snap target pick: %s",
|
|
ToString(*inlinePick).c_str());
|
|
}
|
|
}
|
|
if (aSnapInfo.StrictnessBlock(aWM) != StyleScrollSnapStrictness::None) {
|
|
pickFromBlock();
|
|
if (blockPick && MOZ_LOG_TEST(sApzScrollSnapLog, LogLevel::Debug)) {
|
|
SCROLL_SNAP_LOG("Block snap target pick: %s",
|
|
ToString(*blockPick).c_str());
|
|
}
|
|
}
|
|
|
|
return {x, y};
|
|
}
|
|
|
|
Maybe<SnapDestination> ScrollSnapUtils::GetSnapPointForResnap(
|
|
const ScrollSnapInfo& aSnapInfo, const nsRect& aScrollRange,
|
|
const nsPoint& aCurrentPosition,
|
|
const UniquePtr<ScrollSnapTargetIds>& aLastSnapTargetIds,
|
|
const nsIContent* aFocusedContent, const nsIContent* aTargetContent,
|
|
const WritingMode aWritingMode) {
|
|
if (!aLastSnapTargetIds) {
|
|
return GetSnapPointForDestination(aSnapInfo, ScrollUnit::DEVICE_PIXELS,
|
|
ScrollSnapFlags::IntendedEndPosition,
|
|
aScrollRange, aCurrentPosition,
|
|
aCurrentPosition);
|
|
}
|
|
|
|
auto [x, y] =
|
|
GetCandidateInLastTargets(aSnapInfo, aCurrentPosition, aLastSnapTargetIds,
|
|
aFocusedContent, aTargetContent, aWritingMode);
|
|
if (!x && !y) {
|
|
// In the worst case there's no longer valid snap points previously snapped,
|
|
// try to find new valid snap points.
|
|
return GetSnapPointForDestination(aSnapInfo, ScrollUnit::DEVICE_PIXELS,
|
|
ScrollSnapFlags::IntendedEndPosition,
|
|
aScrollRange, aCurrentPosition,
|
|
aCurrentPosition);
|
|
}
|
|
|
|
// If there's no candidate on one of the axes in the last snap points, try
|
|
// to find a new candidate.
|
|
if (!x || !y) {
|
|
nsPoint newPosition =
|
|
nsPoint(x ? *x : aCurrentPosition.x, y ? *y : aCurrentPosition.y);
|
|
CalcSnapPoints calcSnapPoints(
|
|
ScrollUnit::DEVICE_PIXELS, ScrollSnapFlags::IntendedEndPosition,
|
|
newPosition, newPosition, aSnapInfo.mScrollSnapStrictnessX,
|
|
aSnapInfo.mScrollSnapStrictnessY);
|
|
aSnapInfo.ForEachValidTargetFor(
|
|
newPosition, [&, &x = x, &y = y](const SnapTarget& aTarget) -> bool {
|
|
if (!x && aTarget.mSnapPoint.mX &&
|
|
aSnapInfo.mScrollSnapStrictnessX !=
|
|
StyleScrollSnapStrictness::None) {
|
|
calcSnapPoints.AddVerticalEdge(aTarget);
|
|
}
|
|
if (!y && aTarget.mSnapPoint.mY &&
|
|
aSnapInfo.mScrollSnapStrictnessY !=
|
|
StyleScrollSnapStrictness::None) {
|
|
calcSnapPoints.AddHorizontalEdge(aTarget);
|
|
}
|
|
return true;
|
|
});
|
|
|
|
auto finalPos = calcSnapPoints.GetBestEdge(aSnapInfo.mSnapportSize);
|
|
if (!x) {
|
|
x = Some(finalPos.mPosition.x);
|
|
}
|
|
if (!y) {
|
|
y = Some(finalPos.mPosition.y);
|
|
}
|
|
}
|
|
|
|
SnapDestination snapTarget{nsPoint(*x, *y)};
|
|
// Collect snap points where the position is still same as the new snap
|
|
// position.
|
|
aSnapInfo.ForEachValidTargetFor(
|
|
snapTarget.mPosition,
|
|
[&, &x = x, &y = y](const SnapTarget& aTarget) -> bool {
|
|
if (aTarget.mSnapPoint.mX &&
|
|
aSnapInfo.mScrollSnapStrictnessX !=
|
|
StyleScrollSnapStrictness::None &&
|
|
aTarget.mSnapPoint.mX == x &&
|
|
// Ignore snap points outside of the snapport to optimize the next
|
|
// re-snap iteration.
|
|
aTarget.mSnapArea.Intersects(
|
|
nsRect(nsPoint(*x, *y), aSnapInfo.mSnapportSize))) {
|
|
snapTarget.mTargetIds.mIdsOnX.AppendElement(aTarget.mTargetId);
|
|
}
|
|
|
|
if (aTarget.mSnapPoint.mY &&
|
|
aSnapInfo.mScrollSnapStrictnessY !=
|
|
StyleScrollSnapStrictness::None &&
|
|
aTarget.mSnapPoint.mY == y &&
|
|
// Likewise for the Y axis.
|
|
aTarget.mSnapArea.Intersects(
|
|
nsRect(nsPoint(*x, *y), aSnapInfo.mSnapportSize))) {
|
|
snapTarget.mTargetIds.mIdsOnY.AppendElement(aTarget.mTargetId);
|
|
}
|
|
return true;
|
|
});
|
|
return Some(snapTarget);
|
|
}
|
|
|
|
void ScrollSnapUtils::PostPendingResnapIfNeededFor(nsIFrame* aFrame) {
|
|
MOZ_ASSERT(aFrame);
|
|
|
|
ScrollSnapTargetId id = GetTargetIdFor(aFrame);
|
|
if (id == ScrollSnapTargetId::None) {
|
|
return;
|
|
}
|
|
|
|
ScrollContainerFrame* sf = nsLayoutUtils::GetNearestScrollContainerFrame(
|
|
aFrame, nsLayoutUtils::SCROLLABLE_SAME_DOC |
|
|
nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
|
|
if (!sf) {
|
|
return;
|
|
}
|
|
|
|
sf->PostPendingResnapIfNeeded(aFrame);
|
|
|
|
nsIContent* focusedContent =
|
|
aFrame->PresContext()->Document()->GetUnretargetedFocusedContent(
|
|
dom::Document::IncludeChromeOnly::No);
|
|
// If the focused content is a descendant of |aFrame|, ancestor scroll
|
|
// containers may also need to re-snap since |sf| or other ancestors may be
|
|
// registered as their snap target.
|
|
if (!focusedContent || !nsContentUtils::ContentIsFlattenedTreeDescendantOf(
|
|
focusedContent, aFrame->GetContent())) {
|
|
return;
|
|
}
|
|
|
|
AutoTArray<nsIFrame*, 2> targets = {sf};
|
|
for (nsIFrame* f = sf->GetParent(); f; f = f->GetParent()) {
|
|
if (ScrollContainerFrame* ancestorSf = do_QueryFrame(f)) {
|
|
for (nsIFrame* target : targets) {
|
|
ancestorSf->PostPendingResnapIfNeeded(target);
|
|
}
|
|
targets.ClearAndRetainStorage();
|
|
}
|
|
targets.AppendElement(f);
|
|
}
|
|
}
|
|
|
|
void ScrollSnapUtils::PostPendingResnapFor(nsIFrame* aFrame) {
|
|
if (ScrollContainerFrame* sf = nsLayoutUtils::GetNearestScrollContainerFrame(
|
|
aFrame, nsLayoutUtils::SCROLLABLE_SAME_DOC |
|
|
nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN)) {
|
|
sf->PostPendingResnap();
|
|
}
|
|
}
|
|
|
|
bool ScrollSnapUtils::NeedsToRespectTargetWritingMode(
|
|
const nsSize& aSnapAreaSize, const nsSize& aSnapportSize) {
|
|
// Use the writing-mode on the target element if the snap area is larger than
|
|
// the snapport.
|
|
// https://drafts.csswg.org/css-scroll-snap/#snap-scope
|
|
//
|
|
// It's unclear `larger` means that the size is larger than only on the target
|
|
// axis. If it doesn't, it will pick the same axis in the case where only one
|
|
// axis is larger. For example, if an element size is (200 x 10) and the
|
|
// snapport size is (100 x 100) and if the element's writing mode is different
|
|
// from the scroller's writing mode, then `scroll-snap-align: start start`
|
|
// will be conflict.
|
|
return aSnapAreaSize.width > aSnapportSize.width ||
|
|
aSnapAreaSize.height > aSnapportSize.height;
|
|
}
|
|
|
|
static nsRect InflateByScrollMargin(const nsRect& aTargetRect,
|
|
const nsMargin& aScrollMargin,
|
|
const nsRect& aScrolledRect) {
|
|
// Inflate the rect by scroll-margin.
|
|
nsRect result = aTargetRect;
|
|
result.Inflate(aScrollMargin);
|
|
|
|
// But don't be beyond the limit boundary.
|
|
return result.Intersect(aScrolledRect);
|
|
}
|
|
|
|
nsRect ScrollSnapUtils::GetSnapAreaFor(const nsIFrame* aFrame,
|
|
const nsIFrame* aScrolledFrame,
|
|
const nsRect& aScrolledRect) {
|
|
nsRect targetRect = nsLayoutUtils::TransformFrameRectToAncestor(
|
|
aFrame, aFrame->GetRectRelativeToSelf(), aScrolledFrame);
|
|
|
|
// The snap area contains scroll-margin values.
|
|
// https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-area
|
|
nsMargin scrollMargin = aFrame->StyleMargin()->GetScrollMargin();
|
|
return InflateByScrollMargin(targetRect, scrollMargin, aScrolledRect);
|
|
}
|
|
|
|
} // namespace mozilla
|