1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "ScrollSnap.h"
9 #include "FrameMetrics.h"
11 #include "mozilla/ScrollSnapInfo.h"
12 #include "mozilla/ServoStyleConsts.h"
14 #include "nsIScrollableFrame.h"
15 #include "nsLayoutUtils.h"
16 #include "nsPresContext.h"
18 #include "mozilla/StaticPrefs_layout.h"
23 * Keeps track of the current best edge to snap to. The criteria for
24 * adding an edge depends on the scrolling unit.
26 class CalcSnapPoints final
{
27 using SnapTarget
= ScrollSnapInfo::SnapTarget
;
30 CalcSnapPoints(ScrollUnit aUnit
, ScrollSnapFlags aSnapFlags
,
31 const nsPoint
& aDestination
, const nsPoint
& aStartPos
);
32 struct SnapPosition
: public SnapTarget
{
33 SnapPosition(const SnapTarget
& aSnapTarget
, nscoord aPosition
,
34 nscoord aDistanceOnOtherAxis
)
35 : SnapTarget(aSnapTarget
),
37 mDistanceOnOtherAxis(aDistanceOnOtherAxis
) {}
40 // The distance from the scroll destination to this snap position on the
41 // other axis. This value is used if there are multiple SnapPositions on
42 // this axis, but the positions on the other axis are different.
43 nscoord mDistanceOnOtherAxis
;
46 void AddHorizontalEdge(const SnapTarget
& aTarget
);
47 void AddVerticalEdge(const SnapTarget
& aTarget
);
49 struct CandidateTracker
{
50 // keeps track of the position of the current second best edge on the
51 // opposite side of the best edge on this axis.
52 // We use NSCoordSaturatingSubtract to calculate the distance between a
53 // given position and this second best edge position so that it can be an
54 // uninitialized value as the maximum possible value, because the first
55 // distance calculation would always be nscoord_MAX.
56 nscoord mSecondBestEdge
= nscoord_MAX
;
58 // Assuming in most cases there's no multiple coincide snap points.
59 AutoTArray
<ScrollSnapTargetId
, 1> mTargetIds
;
60 // keeps track of the positions of the current best edge on this axis.
61 // NOTE: Each SnapPosition.mPosition points the same snap position on this
62 // axis but other member variables of SnapPosition may have different
64 AutoTArray
<SnapPosition
, 1> mBestEdges
;
65 bool EdgeFound() const { return !mBestEdges
.IsEmpty(); }
67 void AddEdge(const SnapPosition
& aEdge
, nscoord aDestination
,
68 nscoord aStartPos
, nscoord aScrollingDirection
,
69 CandidateTracker
* aCandidateTracker
);
70 SnapDestination
GetBestEdge(const nsSize
& aSnapportSize
) const;
71 nscoord
XDistanceBetweenBestAndSecondEdge() const {
72 return std::abs(NSCoordSaturatingSubtract(
73 mTrackerOnX
.mSecondBestEdge
,
74 mTrackerOnX
.EdgeFound() ? mTrackerOnX
.mBestEdges
[0].mPosition
78 nscoord
YDistanceBetweenBestAndSecondEdge() const {
79 return std::abs(NSCoordSaturatingSubtract(
80 mTrackerOnY
.mSecondBestEdge
,
81 mTrackerOnY
.EdgeFound() ? mTrackerOnY
.mBestEdges
[0].mPosition
85 const nsPoint
& Destination() const { return mDestination
; }
89 ScrollSnapFlags mSnapFlags
;
90 nsPoint mDestination
; // gives the position after scrolling but before
92 nsPoint mStartPos
; // gives the position before scrolling
93 nsIntPoint mScrollingDirection
; // always -1, 0, or 1
94 CandidateTracker mTrackerOnX
;
95 CandidateTracker mTrackerOnY
;
98 CalcSnapPoints::CalcSnapPoints(ScrollUnit aUnit
, ScrollSnapFlags aSnapFlags
,
99 const nsPoint
& aDestination
,
100 const nsPoint
& aStartPos
)
102 mSnapFlags(aSnapFlags
),
103 mDestination(aDestination
),
104 mStartPos(aStartPos
) {
105 MOZ_ASSERT(aSnapFlags
!= ScrollSnapFlags::Disabled
);
107 nsPoint direction
= aDestination
- aStartPos
;
108 mScrollingDirection
= nsIntPoint(0, 0);
109 if (direction
.x
< 0) {
110 mScrollingDirection
.x
= -1;
112 if (direction
.x
> 0) {
113 mScrollingDirection
.x
= 1;
115 if (direction
.y
< 0) {
116 mScrollingDirection
.y
= -1;
118 if (direction
.y
> 0) {
119 mScrollingDirection
.y
= 1;
123 SnapDestination
CalcSnapPoints::GetBestEdge(const nsSize
& aSnapportSize
) const {
124 if (mTrackerOnX
.EdgeFound() && mTrackerOnY
.EdgeFound()) {
125 nsPoint
bestCandidate(mTrackerOnX
.mBestEdges
[0].mPosition
,
126 mTrackerOnY
.mBestEdges
[0].mPosition
);
127 nsRect snappedPort
= nsRect(bestCandidate
, aSnapportSize
);
129 // If we've found the candidates on both axes, it's possible some of
130 // candidates will be outside of the snapport if we snap to the point
131 // (mTrackerOnX.mBestEdges[0].mPosition,
132 // mTrackerOnY.mBestEdges[0].mPosition). So we need to get the intersection
133 // of the snap area of each snap target element on each axis and the
134 // snapport to tell whether it's outside of the snapport or not.
136 // Also if at least either one of the elements will be outside of the
137 // snapport if we snap to (mTrackerOnX.mBestEdges[0].mPosition,
138 // mTrackerOnY.mBestEdges[0].mPosition). We need to choose one of
139 // combinations of the candidates which is closest to the destination.
141 // So here we iterate over mTrackerOnX and mTrackerOnY just once
142 // respectively for both purposes to avoid iterating over them again and
145 // NOTE: Ideally we have to iterate over every possible combinations of
146 // (mTrackerOnX.mBestEdges[i].mSnapPoint.mY,
147 // mTrackerOnY.mBestEdges[j].mSnapPoint.mX) and tell whether the given
148 // combination will be visible in the snapport or not (maybe we should
149 // choose the one that the visible area, i.e., the intersection area of
150 // the snap target elements and the snapport, is the largest one rather than
151 // the closest one?). But it will be inefficient, so here we will not
152 // iterate all the combinations, we just iterate all the snap target
153 // elements in each axis respectively.
155 AutoTArray
<ScrollSnapTargetId
, 1> visibleTargetIdsOnX
;
156 nscoord minimumDistanceOnY
= nscoord_MAX
;
157 size_t minimumXIndex
= 0;
158 AutoTArray
<ScrollSnapTargetId
, 1> minimumDistanceTargetIdsOnX
;
159 for (size_t i
= 0; i
< mTrackerOnX
.mBestEdges
.Length(); i
++) {
160 const auto& targetX
= mTrackerOnX
.mBestEdges
[i
];
161 if (targetX
.mSnapArea
.Intersects(snappedPort
)) {
162 visibleTargetIdsOnX
.AppendElement(targetX
.mTargetId
);
165 if (targetX
.mDistanceOnOtherAxis
< minimumDistanceOnY
) {
166 minimumDistanceOnY
= targetX
.mDistanceOnOtherAxis
;
168 minimumDistanceTargetIdsOnX
=
169 AutoTArray
<ScrollSnapTargetId
, 1>{targetX
.mTargetId
};
170 } else if (minimumDistanceOnY
!= nscoord_MAX
&&
171 targetX
.mDistanceOnOtherAxis
== minimumDistanceOnY
) {
172 minimumDistanceTargetIdsOnX
.AppendElement(targetX
.mTargetId
);
176 AutoTArray
<ScrollSnapTargetId
, 1> visibleTargetIdsOnY
;
177 nscoord minimumDistanceOnX
= nscoord_MAX
;
178 size_t minimumYIndex
= 0;
179 AutoTArray
<ScrollSnapTargetId
, 1> minimumDistanceTargetIdsOnY
;
180 for (size_t i
= 0; i
< mTrackerOnY
.mBestEdges
.Length(); i
++) {
181 const auto& targetY
= mTrackerOnY
.mBestEdges
[i
];
182 if (targetY
.mSnapArea
.Intersects(snappedPort
)) {
183 visibleTargetIdsOnY
.AppendElement(targetY
.mTargetId
);
186 if (targetY
.mDistanceOnOtherAxis
< minimumDistanceOnX
) {
187 minimumDistanceOnX
= targetY
.mDistanceOnOtherAxis
;
189 minimumDistanceTargetIdsOnY
=
190 AutoTArray
<ScrollSnapTargetId
, 1>{targetY
.mTargetId
};
191 } else if (minimumDistanceOnX
!= nscoord_MAX
&&
192 targetY
.mDistanceOnOtherAxis
== minimumDistanceOnX
) {
193 minimumDistanceTargetIdsOnY
.AppendElement(targetY
.mTargetId
);
197 // If we have the target ids on both axes, it means the target elements
198 // (ids) specifying the best edge on X axis and the target elements
199 // specifying the best edge on Y axis are visible if we snap to the best
200 // edge. Thus they are valid snap positions.
201 if (!visibleTargetIdsOnX
.IsEmpty() && !visibleTargetIdsOnY
.IsEmpty()) {
202 return SnapDestination
{
204 ScrollSnapTargetIds
{visibleTargetIdsOnX
, visibleTargetIdsOnY
}};
207 // Now we've already known that snapping to
208 // (mTrackerOnX.mBestEdges[0].mPosition,
209 // mTrackerOnY.mBestEdges[0].mPosition) will make all candidates of
210 // mTrackerX or mTrackerY (or both) outside of the snapport. We need to
211 // choose another combination where candidates of both mTrackerX/Y are
212 // inside the snapport.
214 // There are three possibilities;
215 // 1) There's no candidate on X axis in mTrackerOnY (that means
216 // each candidate's scroll-snap-align is `none` on X axis), but there's
217 // any candidate in mTrackerOnX, the closest candidates of mTrackerOnX
219 // 2) There's no candidate on Y axis in mTrackerOnX (that means
220 // each candidate's scroll-snap-align is `none` on Y axis), but there's
221 // any candidate in mTrackerOnY, the closest candidates of mTrackerOnY
223 // 3) There are candidates on both axes. Choosing a combination such as
224 // (mTrackerOnX.mBestEdges[i].mSnapPoint.mX,
225 // mTrackerOnY.mBestEdges[i].mSnapPoint.mY)
226 // would require us to iterate over the candidates again if the
227 // combination position is outside the snapport, which we don't want to
228 // do. Instead, we choose either one of the axis' candidates.
229 if ((minimumDistanceOnX
== nscoord_MAX
) &&
230 minimumDistanceOnY
!= nscoord_MAX
) {
231 bestCandidate
.y
= *mTrackerOnX
.mBestEdges
[minimumXIndex
].mSnapPoint
.mY
;
232 return SnapDestination
{bestCandidate
,
233 ScrollSnapTargetIds
{minimumDistanceTargetIdsOnX
,
234 minimumDistanceTargetIdsOnX
}};
237 if (minimumDistanceOnX
!= nscoord_MAX
&&
238 minimumDistanceOnY
== nscoord_MAX
) {
239 bestCandidate
.x
= *mTrackerOnY
.mBestEdges
[minimumYIndex
].mSnapPoint
.mX
;
240 return SnapDestination
{bestCandidate
,
241 ScrollSnapTargetIds
{minimumDistanceTargetIdsOnY
,
242 minimumDistanceTargetIdsOnY
}};
245 if (minimumDistanceOnX
!= nscoord_MAX
&&
246 minimumDistanceOnY
!= nscoord_MAX
) {
247 // If we've found candidates on both axes, choose the closest point either
248 // on X axis or Y axis from the scroll destination. I.e. choose
249 // `minimumXIndex` one or `minimumYIndex` one to make at least one of
250 // snap target elements visible inside the snapport.
253 // [bestCandidate.x, mTrackerOnX.mBestEdges[minimumXIndex].mSnapPoint.mY]
254 // is a candidate generated from a single element, thus snapping to the
255 // point would definitely make the element visible inside the snapport.
256 if (hypotf(NSCoordToFloat(mDestination
.x
-
257 mTrackerOnX
.mBestEdges
[0].mPosition
),
258 NSCoordToFloat(minimumDistanceOnY
)) <
259 hypotf(NSCoordToFloat(minimumDistanceOnX
),
260 NSCoordToFloat(mDestination
.y
-
261 mTrackerOnY
.mBestEdges
[0].mPosition
))) {
262 bestCandidate
.y
= *mTrackerOnX
.mBestEdges
[minimumXIndex
].mSnapPoint
.mY
;
264 bestCandidate
.x
= *mTrackerOnY
.mBestEdges
[minimumYIndex
].mSnapPoint
.mX
;
266 return SnapDestination
{bestCandidate
,
267 ScrollSnapTargetIds
{minimumDistanceTargetIdsOnX
,
268 minimumDistanceTargetIdsOnY
}};
270 MOZ_ASSERT_UNREACHABLE("There's at least one candidate on either axis");
271 // `minimumDistanceOnX == nscoord_MAX && minimumDistanceOnY == nscoord_MAX`
272 // should not happen but we fall back for safety.
275 return SnapDestination
{
277 mTrackerOnX
.EdgeFound() ? mTrackerOnX
.mBestEdges
[0].mPosition
278 // In the case of IntendedEndPosition (i.e. the destination point is
279 // explicitely specied, e.g. scrollTo) use the destination point if we
280 // didn't find any candidates.
281 : !(mSnapFlags
& ScrollSnapFlags::IntendedDirection
) ? mDestination
.x
283 mTrackerOnY
.EdgeFound() ? mTrackerOnY
.mBestEdges
[0].mPosition
284 // Same as above X axis case, use the destination point if we didn't
285 // find any candidates.
286 : !(mSnapFlags
& ScrollSnapFlags::IntendedDirection
) ? mDestination
.y
288 ScrollSnapTargetIds
{mTrackerOnX
.mTargetIds
, mTrackerOnY
.mTargetIds
}};
291 void CalcSnapPoints::AddHorizontalEdge(const SnapTarget
& aTarget
) {
292 MOZ_ASSERT(aTarget
.mSnapPoint
.mY
);
293 AddEdge(SnapPosition
{aTarget
, *aTarget
.mSnapPoint
.mY
,
294 aTarget
.mSnapPoint
.mX
295 ? std::abs(mDestination
.x
- *aTarget
.mSnapPoint
.mX
)
297 mDestination
.y
, mStartPos
.y
, mScrollingDirection
.y
, &mTrackerOnY
);
300 void CalcSnapPoints::AddVerticalEdge(const SnapTarget
& aTarget
) {
301 MOZ_ASSERT(aTarget
.mSnapPoint
.mX
);
302 AddEdge(SnapPosition
{aTarget
, *aTarget
.mSnapPoint
.mX
,
303 aTarget
.mSnapPoint
.mY
304 ? std::abs(mDestination
.y
- *aTarget
.mSnapPoint
.mY
)
306 mDestination
.x
, mStartPos
.x
, mScrollingDirection
.x
, &mTrackerOnX
);
309 void CalcSnapPoints::AddEdge(const SnapPosition
& aEdge
, nscoord aDestination
,
310 nscoord aStartPos
, nscoord aScrollingDirection
,
311 CandidateTracker
* aCandidateTracker
) {
312 if (mSnapFlags
& ScrollSnapFlags::IntendedDirection
) {
313 // In the case of intended direction, we only want to snap to points ahead
314 // of the direction we are scrolling.
315 if (aScrollingDirection
== 0 ||
316 (aEdge
.mPosition
- aStartPos
) * aScrollingDirection
<= 0) {
317 // The scroll direction is neutral - will not hit a snap point, or the
318 // edge is not in the direction we are scrolling, skip it.
323 if (!aCandidateTracker
->EdgeFound()) {
324 aCandidateTracker
->mBestEdges
= AutoTArray
<SnapPosition
, 1>{aEdge
};
325 aCandidateTracker
->mTargetIds
=
326 AutoTArray
<ScrollSnapTargetId
, 1>{aEdge
.mTargetId
};
330 auto isPreferredStopAlways
= [&](const SnapPosition
& aSnapPosition
) -> bool {
331 MOZ_ASSERT(mSnapFlags
& ScrollSnapFlags::IntendedDirection
);
332 // In the case of intended direction scroll operations, `scroll-snap-stop:
333 // always` snap points in between the start point and the scroll destination
334 // are preferable preferable. In other words any `scroll-snap-stop: always`
335 // snap points can be handled as if it's `scroll-snap-stop: normal`.
336 return aSnapPosition
.mScrollSnapStop
== StyleScrollSnapStop::Always
&&
337 std::abs(aSnapPosition
.mPosition
- aStartPos
) <
338 std::abs(aDestination
- aStartPos
);
341 const bool isOnOppositeSide
=
342 ((aEdge
.mPosition
- aDestination
) > 0) !=
343 ((aCandidateTracker
->mBestEdges
[0].mPosition
- aDestination
) > 0);
344 const nscoord distanceFromStart
= aEdge
.mPosition
- aStartPos
;
345 // A utility function to update the best and the second best edges in the
347 // |aIsCloserThanBest| True if the current candidate is closer than the best
349 // |aIsCloserThanSecond| True if the current candidate is closer than
350 // the second best edge.
351 const nscoord distanceFromDestination
= aEdge
.mPosition
- aDestination
;
352 auto updateBestEdges
= [&](bool aIsCloserThanBest
, bool aIsCloserThanSecond
) {
353 if (aIsCloserThanBest
) {
354 if (mSnapFlags
& ScrollSnapFlags::IntendedDirection
&&
355 isPreferredStopAlways(aEdge
)) {
356 // In the case of intended direction scroll operations and the new best
357 // candidate is `scroll-snap-stop: always` and if it's closer to the
358 // start position than the destination, thus we won't use the second
359 // best edge since even if the snap port of the best edge covers entire
360 // snapport, the `scroll-snap-stop: always` snap point is preferred than
362 // NOTE: We've already ignored snap points behind start points so that
363 // we can use std::abs here in the comparison.
365 // For example, if there's a `scroll-snap-stop: always` in between the
366 // start point and destination, no `snap-overflow` mechanism should
367 // happen, if there's `scroll-snap-stop: always` further than the
368 // destination, `snap-overflow` might happen something like below
370 // start always dest other always
371 // |------------|---------|------|
372 aCandidateTracker
->mSecondBestEdge
= aEdge
.mPosition
;
373 } else if (isOnOppositeSide
) {
374 // Replace the second best edge with the current best edge only if the
375 // new best edge (aEdge) is on the opposite side of the current best
377 aCandidateTracker
->mSecondBestEdge
=
378 aCandidateTracker
->mBestEdges
[0].mPosition
;
380 aCandidateTracker
->mBestEdges
= AutoTArray
<SnapPosition
, 1>{aEdge
};
381 aCandidateTracker
->mTargetIds
=
382 AutoTArray
<ScrollSnapTargetId
, 1>{aEdge
.mTargetId
};
384 if (aEdge
.mPosition
== aCandidateTracker
->mBestEdges
[0].mPosition
) {
385 aCandidateTracker
->mTargetIds
.AppendElement(aEdge
.mTargetId
);
386 aCandidateTracker
->mBestEdges
.AppendElement(aEdge
);
388 if (aIsCloserThanSecond
&& isOnOppositeSide
) {
389 aCandidateTracker
->mSecondBestEdge
= aEdge
.mPosition
;
394 bool isCandidateOfBest
= false;
395 bool isCandidateOfSecondBest
= false;
397 case ScrollUnit::DEVICE_PIXELS
:
398 case ScrollUnit::LINES
:
399 case ScrollUnit::WHOLE
: {
401 std::abs(distanceFromDestination
) <
402 std::abs(aCandidateTracker
->mBestEdges
[0].mPosition
- aDestination
);
403 isCandidateOfSecondBest
=
404 std::abs(distanceFromDestination
) <
405 std::abs(NSCoordSaturatingSubtract(aCandidateTracker
->mSecondBestEdge
,
406 aDestination
, nscoord_MAX
));
409 case ScrollUnit::PAGES
: {
410 // distance to the edge from the scrolling destination in the direction of
412 nscoord overshoot
= distanceFromDestination
* aScrollingDirection
;
413 // distance to the current best edge from the scrolling destination in the
414 // direction of scrolling
415 nscoord curOvershoot
=
416 (aCandidateTracker
->mBestEdges
[0].mPosition
- aDestination
) *
419 nscoord secondOvershoot
=
420 NSCoordSaturatingSubtract(aCandidateTracker
->mSecondBestEdge
,
421 aDestination
, nscoord_MAX
) *
424 // edges between the current position and the scrolling destination are
425 // favoured to preserve context
427 isCandidateOfBest
= overshoot
> curOvershoot
|| curOvershoot
>= 0;
428 isCandidateOfSecondBest
=
429 overshoot
> secondOvershoot
|| secondOvershoot
>= 0;
431 // if there are no edges between the current position and the scrolling
432 // destination the closest edge beyond the destination is used
434 isCandidateOfBest
= overshoot
< curOvershoot
;
435 isCandidateOfSecondBest
= overshoot
< secondOvershoot
;
440 if (mSnapFlags
& ScrollSnapFlags::IntendedDirection
) {
441 if (isPreferredStopAlways(aEdge
)) {
442 // If the given position is `scroll-snap-stop: always` and if the position
443 // is in between the start and the destination positions, update the best
444 // position based on the distance from the __start__ point.
446 std::abs(distanceFromStart
) <
447 std::abs(aCandidateTracker
->mBestEdges
[0].mPosition
- aStartPos
);
448 } else if (isPreferredStopAlways(aCandidateTracker
->mBestEdges
[0])) {
449 // If we've found a preferable `scroll-snap-stop:always` position as the
450 // best, do not update it unless the given position is also
451 // `scroll-snap-stop: always`.
452 isCandidateOfBest
= false;
456 updateBestEdges(isCandidateOfBest
, isCandidateOfSecondBest
);
459 static void ProcessSnapPositions(CalcSnapPoints
& aCalcSnapPoints
,
460 const ScrollSnapInfo
& aSnapInfo
) {
461 aSnapInfo
.ForEachValidTargetFor(
462 aCalcSnapPoints
.Destination(), [&](const auto& aTarget
) -> bool {
463 if (aTarget
.mSnapPoint
.mX
&& aSnapInfo
.mScrollSnapStrictnessX
!=
464 StyleScrollSnapStrictness::None
) {
465 aCalcSnapPoints
.AddVerticalEdge(aTarget
);
467 if (aTarget
.mSnapPoint
.mY
&& aSnapInfo
.mScrollSnapStrictnessY
!=
468 StyleScrollSnapStrictness::None
) {
469 aCalcSnapPoints
.AddHorizontalEdge(aTarget
);
475 Maybe
<SnapDestination
> ScrollSnapUtils::GetSnapPointForDestination(
476 const ScrollSnapInfo
& aSnapInfo
, ScrollUnit aUnit
,
477 ScrollSnapFlags aSnapFlags
, const nsRect
& aScrollRange
,
478 const nsPoint
& aStartPos
, const nsPoint
& aDestination
) {
479 if (aSnapInfo
.mScrollSnapStrictnessY
== StyleScrollSnapStrictness::None
&&
480 aSnapInfo
.mScrollSnapStrictnessX
== StyleScrollSnapStrictness::None
) {
484 if (!aSnapInfo
.HasSnapPositions()) {
488 CalcSnapPoints
calcSnapPoints(aUnit
, aSnapFlags
, aDestination
, aStartPos
);
490 ProcessSnapPositions(calcSnapPoints
, aSnapInfo
);
492 // If the distance between the first and the second candidate snap points
493 // is larger than the snapport size and the snapport is covered by larger
494 // elements, any points inside the covering area should be valid snap
496 // https://drafts.csswg.org/css-scroll-snap-1/#snap-overflow
497 // NOTE: |aDestination| sometimes points outside of the scroll range, e.g.
498 // by the APZC fling, so for the overflow checks we need to clamp it.
499 nsPoint clampedDestination
= aScrollRange
.ClampPoint(aDestination
);
500 for (auto range
: aSnapInfo
.mXRangeWiderThanSnapport
) {
501 if (range
.IsValid(clampedDestination
.x
, aSnapInfo
.mSnapportSize
.width
) &&
502 calcSnapPoints
.XDistanceBetweenBestAndSecondEdge() >
503 aSnapInfo
.mSnapportSize
.width
) {
504 calcSnapPoints
.AddVerticalEdge(ScrollSnapInfo::SnapTarget
{
505 Some(clampedDestination
.x
), Nothing(), range
.mSnapArea
,
506 StyleScrollSnapStop::Normal
, range
.mTargetId
});
510 for (auto range
: aSnapInfo
.mYRangeWiderThanSnapport
) {
511 if (range
.IsValid(clampedDestination
.y
, aSnapInfo
.mSnapportSize
.height
) &&
512 calcSnapPoints
.YDistanceBetweenBestAndSecondEdge() >
513 aSnapInfo
.mSnapportSize
.height
) {
514 calcSnapPoints
.AddHorizontalEdge(ScrollSnapInfo::SnapTarget
{
515 Nothing(), Some(clampedDestination
.y
), range
.mSnapArea
,
516 StyleScrollSnapStop::Normal
, range
.mTargetId
});
521 bool snapped
= false;
522 auto finalPos
= calcSnapPoints
.GetBestEdge(aSnapInfo
.mSnapportSize
);
523 constexpr float proximityRatio
= 0.3;
524 if (aSnapInfo
.mScrollSnapStrictnessY
==
525 StyleScrollSnapStrictness::Proximity
&&
526 std::abs(aDestination
.y
- finalPos
.mPosition
.y
) >
527 aSnapInfo
.mSnapportSize
.height
* proximityRatio
) {
528 finalPos
.mPosition
.y
= aDestination
.y
;
529 } else if (aSnapInfo
.mScrollSnapStrictnessY
!=
530 StyleScrollSnapStrictness::None
&&
531 aDestination
.y
!= finalPos
.mPosition
.y
) {
534 if (aSnapInfo
.mScrollSnapStrictnessX
==
535 StyleScrollSnapStrictness::Proximity
&&
536 std::abs(aDestination
.x
- finalPos
.mPosition
.x
) >
537 aSnapInfo
.mSnapportSize
.width
* proximityRatio
) {
538 finalPos
.mPosition
.x
= aDestination
.x
;
539 } else if (aSnapInfo
.mScrollSnapStrictnessX
!=
540 StyleScrollSnapStrictness::None
&&
541 aDestination
.x
!= finalPos
.mPosition
.x
) {
544 return snapped
? Some(finalPos
) : Nothing();
547 ScrollSnapTargetId
ScrollSnapUtils::GetTargetIdFor(const nsIFrame
* aFrame
) {
548 MOZ_ASSERT(aFrame
&& aFrame
->GetContent());
549 return ScrollSnapTargetId
{reinterpret_cast<uintptr_t>(aFrame
->GetContent())};
552 static std::pair
<Maybe
<nscoord
>, Maybe
<nscoord
>> GetCandidateInLastTargets(
553 const ScrollSnapInfo
& aSnapInfo
, const nsPoint
& aCurrentPosition
,
554 const UniquePtr
<ScrollSnapTargetIds
>& aLastSnapTargetIds
,
555 const nsIContent
* aFocusedContent
) {
556 ScrollSnapTargetId targetIdForFocusedContent
= ScrollSnapTargetId::None
;
557 if (aFocusedContent
&& aFocusedContent
->GetPrimaryFrame()) {
558 targetIdForFocusedContent
=
559 ScrollSnapUtils::GetTargetIdFor(aFocusedContent
->GetPrimaryFrame());
562 // Note: Below algorithm doesn't care about cases where the last snap point
563 // was on an element larger than the snapport since it's not clear to us
564 // what we should do for now.
565 // https://github.com/w3c/csswg-drafts/issues/7438
566 const ScrollSnapInfo::SnapTarget
* focusedTarget
= nullptr;
568 aSnapInfo
.ForEachValidTargetFor(
569 aCurrentPosition
, [&](const auto& aTarget
) -> bool {
570 if (aTarget
.mSnapPoint
.mX
&& aSnapInfo
.mScrollSnapStrictnessX
!=
571 StyleScrollSnapStrictness::None
) {
572 if (aLastSnapTargetIds
->mIdsOnX
.Contains(aTarget
.mTargetId
)) {
573 if (targetIdForFocusedContent
== aTarget
.mTargetId
) {
574 // If we've already found the candidate on Y axis, but if snapping
575 // to the point results this target is scrolled out, we can't use
577 if ((y
&& !aTarget
.mSnapArea
.Intersects(
578 nsRect(nsPoint(*aTarget
.mSnapPoint
.mX
, *y
),
579 aSnapInfo
.mSnapportSize
)))) {
583 focusedTarget
= &aTarget
;
584 // If the focused one is valid, then it's the candidate.
585 x
= aTarget
.mSnapPoint
.mX
;
589 // Update the candidate on X axis only if
590 // 1) we haven't yet found the candidate on Y axis
591 // 2) or if we've found the candiate on Y axis and if snapping to
593 // candidate position result the target element is visible
594 // inside the snapport.
595 if (!y
|| (y
&& aTarget
.mSnapArea
.Intersects(
596 nsRect(nsPoint(*aTarget
.mSnapPoint
.mX
, *y
),
597 aSnapInfo
.mSnapportSize
)))) {
598 x
= aTarget
.mSnapPoint
.mX
;
603 if (aTarget
.mSnapPoint
.mY
&& aSnapInfo
.mScrollSnapStrictnessY
!=
604 StyleScrollSnapStrictness::None
) {
605 if (aLastSnapTargetIds
->mIdsOnY
.Contains(aTarget
.mTargetId
)) {
606 if (targetIdForFocusedContent
== aTarget
.mTargetId
) {
608 !focusedTarget
|| focusedTarget
== &aTarget
,
609 "If the focused target has been found on X axis, the "
610 "target should be same");
611 // If we've already found the candidate on X axis other than the
612 // focused one, but if snapping to the point results this target
613 // is scrolled out, we can't use it.
614 if (!focusedTarget
&&
615 (x
&& !aTarget
.mSnapArea
.Intersects(
616 nsRect(nsPoint(*x
, *aTarget
.mSnapPoint
.mY
),
617 aSnapInfo
.mSnapportSize
)))) {
621 focusedTarget
= &aTarget
;
622 y
= aTarget
.mSnapPoint
.mY
;
626 if (!x
|| (x
&& aTarget
.mSnapArea
.Intersects(
627 nsRect(nsPoint(*x
, *aTarget
.mSnapPoint
.mY
),
628 aSnapInfo
.mSnapportSize
)))) {
629 y
= aTarget
.mSnapPoint
.mY
;
635 // If we found candidates on both axes, it's the one we need.
637 // If we haven't found the focused target, it's possible that we
638 // haven't iterated it, don't break in such case.
639 (targetIdForFocusedContent
== ScrollSnapTargetId::None
||
649 Maybe
<SnapDestination
> ScrollSnapUtils::GetSnapPointForResnap(
650 const ScrollSnapInfo
& aSnapInfo
, const nsRect
& aScrollRange
,
651 const nsPoint
& aCurrentPosition
,
652 const UniquePtr
<ScrollSnapTargetIds
>& aLastSnapTargetIds
,
653 const nsIContent
* aFocusedContent
) {
654 if (!aLastSnapTargetIds
) {
655 return GetSnapPointForDestination(aSnapInfo
, ScrollUnit::DEVICE_PIXELS
,
656 ScrollSnapFlags::IntendedEndPosition
,
657 aScrollRange
, aCurrentPosition
,
661 auto [x
, y
] = GetCandidateInLastTargets(aSnapInfo
, aCurrentPosition
,
662 aLastSnapTargetIds
, aFocusedContent
);
664 // In the worst case there's no longer valid snap points previously snapped,
665 // try to find new valid snap points.
666 return GetSnapPointForDestination(aSnapInfo
, ScrollUnit::DEVICE_PIXELS
,
667 ScrollSnapFlags::IntendedEndPosition
,
668 aScrollRange
, aCurrentPosition
,
672 // If there's no candidate on one of the axes in the last snap points, try
673 // to find a new candidate.
675 nsPoint newPosition
=
676 nsPoint(x
? *x
: aCurrentPosition
.x
, y
? *y
: aCurrentPosition
.y
);
677 CalcSnapPoints
calcSnapPoints(ScrollUnit::DEVICE_PIXELS
,
678 ScrollSnapFlags::IntendedEndPosition
,
679 newPosition
, newPosition
);
681 aSnapInfo
.ForEachValidTargetFor(
682 newPosition
, [&, &x
= x
, &y
= y
](const auto& aTarget
) -> bool {
683 if (!x
&& aTarget
.mSnapPoint
.mX
&&
684 aSnapInfo
.mScrollSnapStrictnessX
!=
685 StyleScrollSnapStrictness::None
) {
686 calcSnapPoints
.AddVerticalEdge(aTarget
);
688 if (!y
&& aTarget
.mSnapPoint
.mY
&&
689 aSnapInfo
.mScrollSnapStrictnessY
!=
690 StyleScrollSnapStrictness::None
) {
691 calcSnapPoints
.AddHorizontalEdge(aTarget
);
696 auto finalPos
= calcSnapPoints
.GetBestEdge(aSnapInfo
.mSnapportSize
);
698 x
= Some(finalPos
.mPosition
.x
);
701 y
= Some(finalPos
.mPosition
.y
);
705 SnapDestination snapTarget
{nsPoint(*x
, *y
)};
706 // Collect snap points where the position is still same as the new snap
708 aSnapInfo
.ForEachValidTargetFor(
709 snapTarget
.mPosition
, [&, &x
= x
, &y
= y
](const auto& aTarget
) -> bool {
710 if (aTarget
.mSnapPoint
.mX
&&
711 aSnapInfo
.mScrollSnapStrictnessX
!=
712 StyleScrollSnapStrictness::None
&&
713 aTarget
.mSnapPoint
.mX
== x
) {
714 snapTarget
.mTargetIds
.mIdsOnX
.AppendElement(aTarget
.mTargetId
);
717 if (aTarget
.mSnapPoint
.mY
&&
718 aSnapInfo
.mScrollSnapStrictnessY
!=
719 StyleScrollSnapStrictness::None
&&
720 aTarget
.mSnapPoint
.mY
== y
) {
721 snapTarget
.mTargetIds
.mIdsOnY
.AppendElement(aTarget
.mTargetId
);
725 return Some(snapTarget
);
728 void ScrollSnapUtils::PostPendingResnapIfNeededFor(nsIFrame
* aFrame
) {
729 ScrollSnapTargetId id
= GetTargetIdFor(aFrame
);
730 if (id
== ScrollSnapTargetId::None
) {
734 if (nsIScrollableFrame
* sf
= nsLayoutUtils::GetNearestScrollableFrame(
735 aFrame
, nsLayoutUtils::SCROLLABLE_SAME_DOC
|
736 nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN
)) {
737 sf
->PostPendingResnapIfNeeded(aFrame
);
741 void ScrollSnapUtils::PostPendingResnapFor(nsIFrame
* aFrame
) {
742 if (nsIScrollableFrame
* sf
= nsLayoutUtils::GetNearestScrollableFrame(
743 aFrame
, nsLayoutUtils::SCROLLABLE_SAME_DOC
|
744 nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN
)) {
745 sf
->PostPendingResnap();
749 bool ScrollSnapUtils::NeedsToRespectTargetWritingMode(
750 const nsSize
& aSnapAreaSize
, const nsSize
& aSnapportSize
) {
751 // Use the writing-mode on the target element if the snap area is larger than
753 // https://drafts.csswg.org/css-scroll-snap/#snap-scope
755 // It's unclear `larger` means that the size is larger than only on the target
756 // axis. If it doesn't, it will pick the same axis in the case where only one
757 // axis is larger. For example, if an element size is (200 x 10) and the
758 // snapport size is (100 x 100) and if the element's writing mode is different
759 // from the scroller's writing mode, then `scroll-snap-align: start start`
761 return aSnapAreaSize
.width
> aSnapportSize
.width
||
762 aSnapAreaSize
.height
> aSnapportSize
.height
;
765 static nsRect
InflateByScrollMargin(const nsRect
& aTargetRect
,
766 const nsMargin
& aScrollMargin
,
767 const nsRect
& aScrolledRect
) {
768 // Inflate the rect by scroll-margin.
769 nsRect result
= aTargetRect
;
770 result
.Inflate(aScrollMargin
);
772 // But don't be beyond the limit boundary.
773 return result
.Intersect(aScrolledRect
);
776 nsRect
ScrollSnapUtils::GetSnapAreaFor(const nsIFrame
* aFrame
,
777 const nsIFrame
* aScrolledFrame
,
778 const nsRect
& aScrolledRect
) {
779 nsRect targetRect
= nsLayoutUtils::TransformFrameRectToAncestor(
780 aFrame
, aFrame
->GetRectRelativeToSelf(), aScrolledFrame
);
782 // The snap area contains scroll-margin values.
783 // https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-area
784 nsMargin scrollMargin
= aFrame
->StyleMargin()->GetScrollMargin();
785 return InflateByScrollMargin(targetRect
, scrollMargin
, aScrolledRect
);
788 } // namespace mozilla