Bug 1768393 - Drop mBestEdge initialization. r=botond
[gecko.git] / layout / generic / ScrollSnap.cpp
blob07b8a90ee2615c1925a392c1f6a6a686c0815da1
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"
13 #include "nsIFrame.h"
14 #include "nsIScrollableFrame.h"
15 #include "nsLayoutUtils.h"
16 #include "nsPresContext.h"
17 #include "nsTArray.h"
18 #include "mozilla/StaticPrefs_layout.h"
20 namespace mozilla {
22 /**
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 public:
28 CalcSnapPoints(ScrollUnit aUnit, ScrollSnapFlags aSnapFlags,
29 const nsPoint& aDestination, const nsPoint& aStartPos);
30 struct SnapPosition {
31 SnapPosition() = default;
33 SnapPosition(nscoord aPosition, StyleScrollSnapStop aScrollSnapStop,
34 ScrollSnapTargetId aTargetId)
35 : mPosition(aPosition),
36 mScrollSnapStop(aScrollSnapStop),
37 mTargetId(aTargetId) {}
39 nscoord mPosition;
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
70 : mDestination.x,
71 nscoord_MAX));
73 nscoord YDistanceBetweenBestAndSecondEdge() const {
74 return std::abs(NSCoordSaturatingSubtract(
75 mTrackerOnY.mSecondBestEdge,
76 mTrackerOnY.mEdgeFound ? mTrackerOnY.mBestEdge.mPosition
77 : mDestination.y,
78 nscoord_MAX));
80 const nsPoint& Destination() const { return mDestination; }
82 protected:
83 ScrollUnit mUnit;
84 ScrollSnapFlags mSnapFlags;
85 nsPoint mDestination; // gives the position after scrolling but before
86 // snapping
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)
96 : mUnit(aUnit),
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{
120 nsPoint(
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
126 : mStartPos.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
131 : mStartPos.y),
132 ScrollSnapTargetIds{mTrackerOnX.mTargetIds, mTrackerOnY.mTargetIds}};
135 void CalcSnapPoints::AddHorizontalEdge(const SnapPosition& aEdge) {
136 AddEdge(aEdge, mDestination.y, mStartPos.y, mScrollingDirection.y,
137 &mTrackerOnY);
140 void CalcSnapPoints::AddVerticalEdge(const SnapPosition& aEdge) {
141 AddEdge(aEdge, mDestination.x, mStartPos.x, mScrollingDirection.x,
142 &mTrackerOnX);
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.
155 return;
159 if (!aCandidateTracker->mEdgeFound) {
160 aCandidateTracker->mBestEdge = aEdge;
161 aCandidateTracker->mTargetIds =
162 AutoTArray<ScrollSnapTargetId, 1>{aEdge.mTargetId};
163 aCandidateTracker->mEdgeFound = true;
164 return;
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
183 // given conditions.
184 // |aIsCloserThanBest| True if the current candidate is closer than the best
185 // edge.
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
198 // any points.
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
206 // diagram.
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
213 // edge.
214 aCandidateTracker->mSecondBestEdge =
215 aCandidateTracker->mBestEdge.mPosition;
217 aCandidateTracker->mBestEdge = aEdge;
218 aCandidateTracker->mTargetIds =
219 AutoTArray<ScrollSnapTargetId, 1>{aEdge.mTargetId};
220 } else {
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;
232 switch (mUnit) {
233 case ScrollUnit::DEVICE_PIXELS:
234 case ScrollUnit::LINES:
235 case ScrollUnit::WHOLE: {
236 isCandidateOfBest =
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));
243 break;
245 case ScrollUnit::PAGES: {
246 // distance to the edge from the scrolling destination in the direction of
247 // scrolling
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) *
253 aScrollingDirection;
255 nscoord secondOvershoot =
256 NSCoordSaturatingSubtract(aCandidateTracker->mSecondBestEdge,
257 aDestination, nscoord_MAX) *
258 aScrollingDirection;
260 // edges between the current position and the scrolling destination are
261 // favoured to preserve context
262 if (overshoot < 0) {
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
269 if (overshoot > 0) {
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.
281 isCandidateOfBest =
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,
303 aTarget.mTargetId});
305 if (aTarget.mSnapPoint.mY && aSnapInfo.mScrollSnapStrictnessY !=
306 StyleScrollSnapStrictness::None) {
307 aCalcSnapPoints.AddHorizontalEdge({*aTarget.mSnapPoint.mY,
308 aTarget.mScrollSnapStop,
309 aTarget.mTargetId});
311 return true;
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) {
321 return Nothing();
324 if (!aSnapInfo.HasSnapPositions()) {
325 return Nothing();
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
335 // points.
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});
346 break;
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});
355 break;
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) {
370 snapped = true;
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) {
380 snapped = true;
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;
405 Maybe<nscoord> x, y;
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
414 // it.
415 if ((y && !aTarget.mSnapArea.Intersects(
416 nsRect(nsPoint(*aTarget.mSnapPoint.mX, *y),
417 aSnapInfo.mSnapportSize)))) {
418 y.reset();
421 focusedTarget = &aTarget;
422 // If the focused one is valid, then it's the candidate.
423 x = aTarget.mSnapPoint.mX;
426 if (!x) {
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
430 // the
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) {
445 NS_ASSERTION(
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)))) {
456 x.reset();
459 focusedTarget = &aTarget;
460 y = aTarget.mSnapPoint.mY;
463 if (!y) {
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.
474 if (x && y &&
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 ||
478 focusedTarget)) {
479 return false;
481 return true;
484 return {x, y};
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,
496 aCurrentPosition);
499 auto [x, y] = GetCandidateInLastTargets(aSnapInfo, aCurrentPosition,
500 aLastSnapTargetIds, aFocusedContent);
501 if (!x && !y) {
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,
507 aCurrentPosition);
510 // If there's no candidate on one of the axes in the last snap points, try
511 // to find a new candidate.
512 if (!x || !y) {
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,
526 aTarget.mTargetId});
528 if (!y && aTarget.mSnapPoint.mY &&
529 aSnapInfo.mScrollSnapStrictnessY !=
530 StyleScrollSnapStrictness::None) {
531 calcSnapPoints.AddHorizontalEdge({*aTarget.mSnapPoint.mY,
532 aTarget.mScrollSnapStop,
533 aTarget.mTargetId});
535 return true;
538 auto finalPos = calcSnapPoints.GetBestEdge();
539 if (!x) {
540 x = Some(finalPos.mPosition.x);
542 if (!y) {
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
549 // position.
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);
565 return true;
567 return Some(snapTarget);
570 void ScrollSnapUtils::PostPendingResnapIfNeededFor(nsIFrame* aFrame) {
571 ScrollSnapTargetId id = GetTargetIdFor(aFrame);
572 if (id == ScrollSnapTargetId::None) {
573 return;
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
594 // the snapport.
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`
602 // will be conflict.
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