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
{
28 CalcSnapPoints(ScrollUnit aUnit
, ScrollSnapFlags aSnapFlags
,
29 const nsPoint
& aDestination
, const nsPoint
& aStartPos
);
31 SnapPosition() = default;
33 SnapPosition(nscoord aPosition
, StyleScrollSnapStop aScrollSnapStop
,
34 ScrollSnapTargetId aTargetId
)
35 : mPosition(aPosition
),
36 mScrollSnapStop(aScrollSnapStop
),
37 mTargetId(aTargetId
) {}
40 StyleScrollSnapStop mScrollSnapStop
;
41 ScrollSnapTargetId mTargetId
;
44 void AddHorizontalEdge(const SnapPosition
& aEdge
);
45 void AddVerticalEdge(const SnapPosition
& aEdge
);
47 struct CandidateTracker
{
48 // keeps track of the position of the current best edge on this axis.
49 SnapPosition mBestEdge
;
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
;
57 bool mEdgeFound
= false; // true if mBestEdge is storing a valid edge.
59 // Assuming in most cases there's no multiple coincide snap points.
60 AutoTArray
<ScrollSnapTargetId
, 1> mTargetIds
;
62 void AddEdge(const SnapPosition
& aEdge
, nscoord aDestination
,
63 nscoord aStartPos
, nscoord aScrollingDirection
,
64 CandidateTracker
* aCandidateTracker
);
65 SnapDestination
GetBestEdge() const;
66 nscoord
XDistanceBetweenBestAndSecondEdge() const {
67 return std::abs(NSCoordSaturatingSubtract(
68 mTrackerOnX
.mSecondBestEdge
,
69 mTrackerOnX
.mEdgeFound
? mTrackerOnX
.mBestEdge
.mPosition
73 nscoord
YDistanceBetweenBestAndSecondEdge() const {
74 return std::abs(NSCoordSaturatingSubtract(
75 mTrackerOnY
.mSecondBestEdge
,
76 mTrackerOnY
.mEdgeFound
? mTrackerOnY
.mBestEdge
.mPosition
80 const nsPoint
& Destination() const { return mDestination
; }
84 ScrollSnapFlags mSnapFlags
;
85 nsPoint mDestination
; // gives the position after scrolling but before
87 nsPoint mStartPos
; // gives the position before scrolling
88 nsIntPoint mScrollingDirection
; // always -1, 0, or 1
89 CandidateTracker mTrackerOnX
;
90 CandidateTracker mTrackerOnY
;
93 CalcSnapPoints::CalcSnapPoints(ScrollUnit aUnit
, ScrollSnapFlags aSnapFlags
,
94 const nsPoint
& aDestination
,
95 const nsPoint
& aStartPos
)
97 mSnapFlags(aSnapFlags
),
98 mDestination(aDestination
),
99 mStartPos(aStartPos
) {
100 MOZ_ASSERT(aSnapFlags
!= ScrollSnapFlags::Disabled
);
102 nsPoint direction
= aDestination
- aStartPos
;
103 mScrollingDirection
= nsIntPoint(0, 0);
104 if (direction
.x
< 0) {
105 mScrollingDirection
.x
= -1;
107 if (direction
.x
> 0) {
108 mScrollingDirection
.x
= 1;
110 if (direction
.y
< 0) {
111 mScrollingDirection
.y
= -1;
113 if (direction
.y
> 0) {
114 mScrollingDirection
.y
= 1;
118 SnapDestination
CalcSnapPoints::GetBestEdge() const {
119 return SnapDestination
{
121 mTrackerOnX
.mEdgeFound
? mTrackerOnX
.mBestEdge
.mPosition
122 // In the case of IntendedEndPosition (i.e. the destination point is
123 // explicitely specied, e.g. scrollTo) use the destination point if we
124 // didn't find any candidates.
125 : !(mSnapFlags
& ScrollSnapFlags::IntendedDirection
) ? mDestination
.x
127 mTrackerOnY
.mEdgeFound
? mTrackerOnY
.mBestEdge
.mPosition
128 // Same as above X axis case, use the destination point if we didn't
129 // find any candidates.
130 : !(mSnapFlags
& ScrollSnapFlags::IntendedDirection
) ? mDestination
.y
132 ScrollSnapTargetIds
{mTrackerOnX
.mTargetIds
, mTrackerOnY
.mTargetIds
}};
135 void CalcSnapPoints::AddHorizontalEdge(const SnapPosition
& aEdge
) {
136 AddEdge(aEdge
, mDestination
.y
, mStartPos
.y
, mScrollingDirection
.y
,
140 void CalcSnapPoints::AddVerticalEdge(const SnapPosition
& aEdge
) {
141 AddEdge(aEdge
, mDestination
.x
, mStartPos
.x
, mScrollingDirection
.x
,
145 void CalcSnapPoints::AddEdge(const SnapPosition
& aEdge
, nscoord aDestination
,
146 nscoord aStartPos
, nscoord aScrollingDirection
,
147 CandidateTracker
* aCandidateTracker
) {
148 if (mSnapFlags
& ScrollSnapFlags::IntendedDirection
) {
149 // In the case of intended direction, we only want to snap to points ahead
150 // of the direction we are scrolling.
151 if (aScrollingDirection
== 0 ||
152 (aEdge
.mPosition
- aStartPos
) * aScrollingDirection
<= 0) {
153 // The scroll direction is neutral - will not hit a snap point, or the
154 // edge is not in the direction we are scrolling, skip it.
159 if (!aCandidateTracker
->mEdgeFound
) {
160 aCandidateTracker
->mBestEdge
= aEdge
;
161 aCandidateTracker
->mTargetIds
=
162 AutoTArray
<ScrollSnapTargetId
, 1>{aEdge
.mTargetId
};
163 aCandidateTracker
->mEdgeFound
= true;
167 auto isPreferredStopAlways
= [&](const SnapPosition
& aSnapPosition
) -> bool {
168 MOZ_ASSERT(mSnapFlags
& ScrollSnapFlags::IntendedDirection
);
169 // In the case of intended direction scroll operations, `scroll-snap-stop:
170 // always` snap points in between the start point and the scroll destination
171 // are preferable preferable. In other words any `scroll-snap-stop: always`
172 // snap points can be handled as if it's `scroll-snap-stop: normal`.
173 return aSnapPosition
.mScrollSnapStop
== StyleScrollSnapStop::Always
&&
174 std::abs(aSnapPosition
.mPosition
- aStartPos
) <
175 std::abs(aDestination
- aStartPos
);
178 const bool isOnOppositeSide
=
179 ((aEdge
.mPosition
- aDestination
) > 0) !=
180 ((aCandidateTracker
->mBestEdge
.mPosition
- aDestination
) > 0);
181 const nscoord distanceFromStart
= aEdge
.mPosition
- aStartPos
;
182 // A utility function to update the best and the second best edges in the
184 // |aIsCloserThanBest| True if the current candidate is closer than the best
186 // |aIsCloserThanSecond| True if the current candidate is closer than
187 // the second best edge.
188 const nscoord distanceFromDestination
= aEdge
.mPosition
- aDestination
;
189 auto updateBestEdges
= [&](bool aIsCloserThanBest
, bool aIsCloserThanSecond
) {
190 if (aIsCloserThanBest
) {
191 if (mSnapFlags
& ScrollSnapFlags::IntendedDirection
&&
192 isPreferredStopAlways(aEdge
)) {
193 // In the case of intended direction scroll operations and the new best
194 // candidate is `scroll-snap-stop: always` and if it's closer to the
195 // start position than the destination, thus we won't use the second
196 // best edge since even if the snap port of the best edge covers entire
197 // snapport, the `scroll-snap-stop: always` snap point is preferred than
199 // NOTE: We've already ignored snap points behind start points so that
200 // we can use std::abs here in the comparison.
202 // For example, if there's a `scroll-snap-stop: always` in between the
203 // start point and destination, no `snap-overflow` mechanism should
204 // happen, if there's `scroll-snap-stop: always` further than the
205 // destination, `snap-overflow` might happen something like below
207 // start always dest other always
208 // |------------|---------|------|
209 aCandidateTracker
->mSecondBestEdge
= aEdge
.mPosition
;
210 } else if (isOnOppositeSide
) {
211 // Replace the second best edge with the current best edge only if the
212 // new best edge (aEdge) is on the opposite side of the current best
214 aCandidateTracker
->mSecondBestEdge
=
215 aCandidateTracker
->mBestEdge
.mPosition
;
217 aCandidateTracker
->mBestEdge
= aEdge
;
218 aCandidateTracker
->mTargetIds
=
219 AutoTArray
<ScrollSnapTargetId
, 1>{aEdge
.mTargetId
};
221 if (aEdge
.mPosition
== aCandidateTracker
->mBestEdge
.mPosition
) {
222 aCandidateTracker
->mTargetIds
.AppendElement(aEdge
.mTargetId
);
224 if (aIsCloserThanSecond
&& isOnOppositeSide
) {
225 aCandidateTracker
->mSecondBestEdge
= aEdge
.mPosition
;
230 bool isCandidateOfBest
= false;
231 bool isCandidateOfSecondBest
= false;
233 case ScrollUnit::DEVICE_PIXELS
:
234 case ScrollUnit::LINES
:
235 case ScrollUnit::WHOLE
: {
237 std::abs(distanceFromDestination
) <
238 std::abs(aCandidateTracker
->mBestEdge
.mPosition
- aDestination
);
239 isCandidateOfSecondBest
=
240 std::abs(distanceFromDestination
) <
241 std::abs(NSCoordSaturatingSubtract(aCandidateTracker
->mSecondBestEdge
,
242 aDestination
, nscoord_MAX
));
245 case ScrollUnit::PAGES
: {
246 // distance to the edge from the scrolling destination in the direction of
248 nscoord overshoot
= distanceFromDestination
* aScrollingDirection
;
249 // distance to the current best edge from the scrolling destination in the
250 // direction of scrolling
251 nscoord curOvershoot
=
252 (aCandidateTracker
->mBestEdge
.mPosition
- aDestination
) *
255 nscoord secondOvershoot
=
256 NSCoordSaturatingSubtract(aCandidateTracker
->mSecondBestEdge
,
257 aDestination
, nscoord_MAX
) *
260 // edges between the current position and the scrolling destination are
261 // favoured to preserve context
263 isCandidateOfBest
= overshoot
> curOvershoot
|| curOvershoot
>= 0;
264 isCandidateOfSecondBest
=
265 overshoot
> secondOvershoot
|| secondOvershoot
>= 0;
267 // if there are no edges between the current position and the scrolling
268 // destination the closest edge beyond the destination is used
270 isCandidateOfBest
= overshoot
< curOvershoot
;
271 isCandidateOfSecondBest
= overshoot
< secondOvershoot
;
276 if (mSnapFlags
& ScrollSnapFlags::IntendedDirection
) {
277 if (isPreferredStopAlways(aEdge
)) {
278 // If the given position is `scroll-snap-stop: always` and if the position
279 // is in between the start and the destination positions, update the best
280 // position based on the distance from the __start__ point.
282 std::abs(distanceFromStart
) <
283 std::abs(aCandidateTracker
->mBestEdge
.mPosition
- aStartPos
);
284 } else if (isPreferredStopAlways(aCandidateTracker
->mBestEdge
)) {
285 // If we've found a preferable `scroll-snap-stop:always` position as the
286 // best, do not update it unless the given position is also
287 // `scroll-snap-stop: always`.
288 isCandidateOfBest
= false;
292 updateBestEdges(isCandidateOfBest
, isCandidateOfSecondBest
);
295 static void ProcessSnapPositions(CalcSnapPoints
& aCalcSnapPoints
,
296 const ScrollSnapInfo
& aSnapInfo
) {
297 aSnapInfo
.ForEachValidTargetFor(
298 aCalcSnapPoints
.Destination(), [&](const auto& aTarget
) -> bool {
299 if (aTarget
.mSnapPoint
.mX
&& aSnapInfo
.mScrollSnapStrictnessX
!=
300 StyleScrollSnapStrictness::None
) {
301 aCalcSnapPoints
.AddVerticalEdge({*aTarget
.mSnapPoint
.mX
,
302 aTarget
.mScrollSnapStop
,
305 if (aTarget
.mSnapPoint
.mY
&& aSnapInfo
.mScrollSnapStrictnessY
!=
306 StyleScrollSnapStrictness::None
) {
307 aCalcSnapPoints
.AddHorizontalEdge({*aTarget
.mSnapPoint
.mY
,
308 aTarget
.mScrollSnapStop
,
315 Maybe
<SnapDestination
> ScrollSnapUtils::GetSnapPointForDestination(
316 const ScrollSnapInfo
& aSnapInfo
, ScrollUnit aUnit
,
317 ScrollSnapFlags aSnapFlags
, const nsRect
& aScrollRange
,
318 const nsPoint
& aStartPos
, const nsPoint
& aDestination
) {
319 if (aSnapInfo
.mScrollSnapStrictnessY
== StyleScrollSnapStrictness::None
&&
320 aSnapInfo
.mScrollSnapStrictnessX
== StyleScrollSnapStrictness::None
) {
324 if (!aSnapInfo
.HasSnapPositions()) {
328 CalcSnapPoints
calcSnapPoints(aUnit
, aSnapFlags
, aDestination
, aStartPos
);
330 ProcessSnapPositions(calcSnapPoints
, aSnapInfo
);
332 // If the distance between the first and the second candidate snap points
333 // is larger than the snapport size and the snapport is covered by larger
334 // elements, any points inside the covering area should be valid snap
336 // https://drafts.csswg.org/css-scroll-snap-1/#snap-overflow
337 // NOTE: |aDestination| sometimes points outside of the scroll range, e.g.
338 // by the APZC fling, so for the overflow checks we need to clamp it.
339 nsPoint clampedDestination
= aScrollRange
.ClampPoint(aDestination
);
340 for (auto range
: aSnapInfo
.mXRangeWiderThanSnapport
) {
341 if (range
.IsValid(clampedDestination
.x
, aSnapInfo
.mSnapportSize
.width
) &&
342 calcSnapPoints
.XDistanceBetweenBestAndSecondEdge() >
343 aSnapInfo
.mSnapportSize
.width
) {
344 calcSnapPoints
.AddVerticalEdge(CalcSnapPoints::SnapPosition
{
345 clampedDestination
.x
, StyleScrollSnapStop::Normal
, range
.mTargetId
});
349 for (auto range
: aSnapInfo
.mYRangeWiderThanSnapport
) {
350 if (range
.IsValid(clampedDestination
.y
, aSnapInfo
.mSnapportSize
.height
) &&
351 calcSnapPoints
.YDistanceBetweenBestAndSecondEdge() >
352 aSnapInfo
.mSnapportSize
.height
) {
353 calcSnapPoints
.AddHorizontalEdge(CalcSnapPoints::SnapPosition
{
354 clampedDestination
.y
, StyleScrollSnapStop::Normal
, range
.mTargetId
});
359 bool snapped
= false;
360 auto finalPos
= calcSnapPoints
.GetBestEdge();
361 constexpr float proximityRatio
= 0.3;
362 if (aSnapInfo
.mScrollSnapStrictnessY
==
363 StyleScrollSnapStrictness::Proximity
&&
364 std::abs(aDestination
.y
- finalPos
.mPosition
.y
) >
365 aSnapInfo
.mSnapportSize
.height
* proximityRatio
) {
366 finalPos
.mPosition
.y
= aDestination
.y
;
367 } else if (aSnapInfo
.mScrollSnapStrictnessY
!=
368 StyleScrollSnapStrictness::None
&&
369 aDestination
.y
!= finalPos
.mPosition
.y
) {
372 if (aSnapInfo
.mScrollSnapStrictnessX
==
373 StyleScrollSnapStrictness::Proximity
&&
374 std::abs(aDestination
.x
- finalPos
.mPosition
.x
) >
375 aSnapInfo
.mSnapportSize
.width
* proximityRatio
) {
376 finalPos
.mPosition
.x
= aDestination
.x
;
377 } else if (aSnapInfo
.mScrollSnapStrictnessX
!=
378 StyleScrollSnapStrictness::None
&&
379 aDestination
.x
!= finalPos
.mPosition
.x
) {
382 return snapped
? Some(finalPos
) : Nothing();
385 ScrollSnapTargetId
ScrollSnapUtils::GetTargetIdFor(const nsIFrame
* aFrame
) {
386 MOZ_ASSERT(aFrame
&& aFrame
->GetContent());
387 return ScrollSnapTargetId
{reinterpret_cast<uintptr_t>(aFrame
->GetContent())};
390 static std::pair
<Maybe
<nscoord
>, Maybe
<nscoord
>> GetCandidateInLastTargets(
391 const ScrollSnapInfo
& aSnapInfo
, const nsPoint
& aCurrentPosition
,
392 const UniquePtr
<ScrollSnapTargetIds
>& aLastSnapTargetIds
,
393 const nsIContent
* aFocusedContent
) {
394 ScrollSnapTargetId targetIdForFocusedContent
= ScrollSnapTargetId::None
;
395 if (aFocusedContent
&& aFocusedContent
->GetPrimaryFrame()) {
396 targetIdForFocusedContent
=
397 ScrollSnapUtils::GetTargetIdFor(aFocusedContent
->GetPrimaryFrame());
400 // Note: Below algorithm doesn't care about cases where the last snap point
401 // was on an element larger than the snapport since it's not clear to us
402 // what we should do for now.
403 // https://github.com/w3c/csswg-drafts/issues/7438
404 const ScrollSnapInfo::SnapTarget
* focusedTarget
= nullptr;
406 aSnapInfo
.ForEachValidTargetFor(
407 aCurrentPosition
, [&](const auto& aTarget
) -> bool {
408 if (aTarget
.mSnapPoint
.mX
&& aSnapInfo
.mScrollSnapStrictnessX
!=
409 StyleScrollSnapStrictness::None
) {
410 if (aLastSnapTargetIds
->mIdsOnX
.Contains(aTarget
.mTargetId
)) {
411 if (targetIdForFocusedContent
== aTarget
.mTargetId
) {
412 // If we've already found the candidate on Y axis, but if snapping
413 // to the point results this target is scrolled out, we can't use
415 if ((y
&& !aTarget
.mSnapArea
.Intersects(
416 nsRect(nsPoint(*aTarget
.mSnapPoint
.mX
, *y
),
417 aSnapInfo
.mSnapportSize
)))) {
421 focusedTarget
= &aTarget
;
422 // If the focused one is valid, then it's the candidate.
423 x
= aTarget
.mSnapPoint
.mX
;
427 // Update the candidate on X axis only if
428 // 1) we haven't yet found the candidate on Y axis
429 // 2) or if we've found the candiate on Y axis and if snapping to
431 // candidate position result the target element is visible
432 // inside the snapport.
433 if (!y
|| (y
&& aTarget
.mSnapArea
.Intersects(
434 nsRect(nsPoint(*aTarget
.mSnapPoint
.mX
, *y
),
435 aSnapInfo
.mSnapportSize
)))) {
436 x
= aTarget
.mSnapPoint
.mX
;
441 if (aTarget
.mSnapPoint
.mY
&& aSnapInfo
.mScrollSnapStrictnessY
!=
442 StyleScrollSnapStrictness::None
) {
443 if (aLastSnapTargetIds
->mIdsOnY
.Contains(aTarget
.mTargetId
)) {
444 if (targetIdForFocusedContent
== aTarget
.mTargetId
) {
446 !focusedTarget
|| focusedTarget
== &aTarget
,
447 "If the focused target has been found on X axis, the "
448 "target should be same");
449 // If we've already found the candidate on X axis other than the
450 // focused one, but if snapping to the point results this target
451 // is scrolled out, we can't use it.
452 if (!focusedTarget
&&
453 (x
&& !aTarget
.mSnapArea
.Intersects(
454 nsRect(nsPoint(*x
, *aTarget
.mSnapPoint
.mY
),
455 aSnapInfo
.mSnapportSize
)))) {
459 focusedTarget
= &aTarget
;
460 y
= aTarget
.mSnapPoint
.mY
;
464 if (!x
|| (x
&& aTarget
.mSnapArea
.Intersects(
465 nsRect(nsPoint(*x
, *aTarget
.mSnapPoint
.mY
),
466 aSnapInfo
.mSnapportSize
)))) {
467 y
= aTarget
.mSnapPoint
.mY
;
473 // If we found candidates on both axes, it's the one we need.
475 // If we haven't found the focused target, it's possible that we
476 // haven't iterated it, don't break in such case.
477 (targetIdForFocusedContent
== ScrollSnapTargetId::None
||
487 Maybe
<SnapDestination
> ScrollSnapUtils::GetSnapPointForResnap(
488 const ScrollSnapInfo
& aSnapInfo
, const nsRect
& aScrollRange
,
489 const nsPoint
& aCurrentPosition
,
490 const UniquePtr
<ScrollSnapTargetIds
>& aLastSnapTargetIds
,
491 const nsIContent
* aFocusedContent
) {
492 if (!aLastSnapTargetIds
) {
493 return GetSnapPointForDestination(aSnapInfo
, ScrollUnit::DEVICE_PIXELS
,
494 ScrollSnapFlags::IntendedEndPosition
,
495 aScrollRange
, aCurrentPosition
,
499 auto [x
, y
] = GetCandidateInLastTargets(aSnapInfo
, aCurrentPosition
,
500 aLastSnapTargetIds
, aFocusedContent
);
502 // In the worst case there's no longer valid snap points previously snapped,
503 // try to find new valid snap points.
504 return GetSnapPointForDestination(aSnapInfo
, ScrollUnit::DEVICE_PIXELS
,
505 ScrollSnapFlags::IntendedEndPosition
,
506 aScrollRange
, aCurrentPosition
,
510 // If there's no candidate on one of the axes in the last snap points, try
511 // to find a new candidate.
513 nsPoint newPosition
=
514 nsPoint(x
? *x
: aCurrentPosition
.x
, y
? *y
: aCurrentPosition
.y
);
515 CalcSnapPoints
calcSnapPoints(ScrollUnit::DEVICE_PIXELS
,
516 ScrollSnapFlags::IntendedEndPosition
,
517 newPosition
, newPosition
);
519 aSnapInfo
.ForEachValidTargetFor(
520 newPosition
, [&, &x
= x
, &y
= y
](const auto& aTarget
) -> bool {
521 if (!x
&& aTarget
.mSnapPoint
.mX
&&
522 aSnapInfo
.mScrollSnapStrictnessX
!=
523 StyleScrollSnapStrictness::None
) {
524 calcSnapPoints
.AddVerticalEdge({*aTarget
.mSnapPoint
.mX
,
525 aTarget
.mScrollSnapStop
,
528 if (!y
&& aTarget
.mSnapPoint
.mY
&&
529 aSnapInfo
.mScrollSnapStrictnessY
!=
530 StyleScrollSnapStrictness::None
) {
531 calcSnapPoints
.AddHorizontalEdge({*aTarget
.mSnapPoint
.mY
,
532 aTarget
.mScrollSnapStop
,
538 auto finalPos
= calcSnapPoints
.GetBestEdge();
540 x
= Some(finalPos
.mPosition
.x
);
543 y
= Some(finalPos
.mPosition
.y
);
547 SnapDestination snapTarget
{nsPoint(*x
, *y
)};
548 // Collect snap points where the position is still same as the new snap
550 aSnapInfo
.ForEachValidTargetFor(
551 snapTarget
.mPosition
, [&, &x
= x
, &y
= y
](const auto& aTarget
) -> bool {
552 if (aTarget
.mSnapPoint
.mX
&&
553 aSnapInfo
.mScrollSnapStrictnessX
!=
554 StyleScrollSnapStrictness::None
&&
555 aTarget
.mSnapPoint
.mX
== x
) {
556 snapTarget
.mTargetIds
.mIdsOnX
.AppendElement(aTarget
.mTargetId
);
559 if (aTarget
.mSnapPoint
.mY
&&
560 aSnapInfo
.mScrollSnapStrictnessY
!=
561 StyleScrollSnapStrictness::None
&&
562 aTarget
.mSnapPoint
.mY
== y
) {
563 snapTarget
.mTargetIds
.mIdsOnY
.AppendElement(aTarget
.mTargetId
);
567 return Some(snapTarget
);
570 void ScrollSnapUtils::PostPendingResnapIfNeededFor(nsIFrame
* aFrame
) {
571 ScrollSnapTargetId id
= GetTargetIdFor(aFrame
);
572 if (id
== ScrollSnapTargetId::None
) {
576 if (nsIScrollableFrame
* sf
= nsLayoutUtils::GetNearestScrollableFrame(
577 aFrame
, nsLayoutUtils::SCROLLABLE_SAME_DOC
|
578 nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN
)) {
579 sf
->PostPendingResnapIfNeeded(aFrame
);
583 void ScrollSnapUtils::PostPendingResnapFor(nsIFrame
* aFrame
) {
584 if (nsIScrollableFrame
* sf
= nsLayoutUtils::GetNearestScrollableFrame(
585 aFrame
, nsLayoutUtils::SCROLLABLE_SAME_DOC
|
586 nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN
)) {
587 sf
->PostPendingResnap();
591 bool ScrollSnapUtils::NeedsToRespectTargetWritingMode(
592 const nsSize
& aSnapAreaSize
, const nsSize
& aSnapportSize
) {
593 // Use the writing-mode on the target element if the snap area is larger than
595 // https://drafts.csswg.org/css-scroll-snap/#snap-scope
597 // It's unclear `larger` means that the size is larger than only on the target
598 // axis. If it doesn't, it will pick the same axis in the case where only one
599 // axis is larger. For example, if an element size is (200 x 10) and the
600 // snapport size is (100 x 100) and if the element's writing mode is different
601 // from the scroller's writing mode, then `scroll-snap-align: start start`
603 return aSnapAreaSize
.width
> aSnapportSize
.width
||
604 aSnapAreaSize
.height
> aSnapportSize
.height
;
607 static nsRect
InflateByScrollMargin(const nsRect
& aTargetRect
,
608 const nsMargin
& aScrollMargin
,
609 const nsRect
& aScrolledRect
) {
610 // Inflate the rect by scroll-margin.
611 nsRect result
= aTargetRect
;
612 result
.Inflate(aScrollMargin
);
614 // But don't be beyond the limit boundary.
615 return result
.Intersect(aScrolledRect
);
618 nsRect
ScrollSnapUtils::GetSnapAreaFor(const nsIFrame
* aFrame
,
619 const nsIFrame
* aScrolledFrame
,
620 const nsRect
& aScrolledRect
) {
621 nsRect targetRect
= nsLayoutUtils::TransformFrameRectToAncestor(
622 aFrame
, aFrame
->GetRectRelativeToSelf(), aScrolledFrame
);
624 // The snap area contains scroll-margin values.
625 // https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-area
626 nsMargin scrollMargin
= aFrame
->StyleMargin()->GetScrollMargin();
627 return InflateByScrollMargin(targetRect
, scrollMargin
, aScrolledRect
);
630 } // namespace mozilla