Bug 1839315: part 4) Link from `SheetLoadData::mWasAlternate` to spec. r=emilio DONTBUILD
[gecko.git] / layout / generic / ScrollSnap.cpp
blobcfe9fb1cbb4f1b14f1ddf882ae0d5db4396931a9
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 using SnapTarget = ScrollSnapInfo::SnapTarget;
29 public:
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),
36 mPosition(aPosition),
37 mDistanceOnOtherAxis(aDistanceOnOtherAxis) {}
39 nscoord mPosition;
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
63 // values.
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
75 : mDestination.x,
76 nscoord_MAX));
78 nscoord YDistanceBetweenBestAndSecondEdge() const {
79 return std::abs(NSCoordSaturatingSubtract(
80 mTrackerOnY.mSecondBestEdge,
81 mTrackerOnY.EdgeFound() ? mTrackerOnY.mBestEdges[0].mPosition
82 : mDestination.y,
83 nscoord_MAX));
85 const nsPoint& Destination() const { return mDestination; }
87 protected:
88 ScrollUnit mUnit;
89 ScrollSnapFlags mSnapFlags;
90 nsPoint mDestination; // gives the position after scrolling but before
91 // snapping
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)
101 : mUnit(aUnit),
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
143 // again.
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;
167 minimumXIndex = i;
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;
188 minimumYIndex = i;
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{
203 bestCandidate,
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
218 // should be used.
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
222 // should be used.
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.
252 // For example,
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;
263 } else {
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{
276 nsPoint(
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
282 : mStartPos.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
287 : mStartPos.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)
296 : nscoord_MAX},
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)
305 : nscoord_MAX},
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.
319 return;
323 if (!aCandidateTracker->EdgeFound()) {
324 aCandidateTracker->mBestEdges = AutoTArray<SnapPosition, 1>{aEdge};
325 aCandidateTracker->mTargetIds =
326 AutoTArray<ScrollSnapTargetId, 1>{aEdge.mTargetId};
327 return;
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
346 // given conditions.
347 // |aIsCloserThanBest| True if the current candidate is closer than the best
348 // edge.
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
361 // any points.
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
369 // diagram.
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
376 // edge.
377 aCandidateTracker->mSecondBestEdge =
378 aCandidateTracker->mBestEdges[0].mPosition;
380 aCandidateTracker->mBestEdges = AutoTArray<SnapPosition, 1>{aEdge};
381 aCandidateTracker->mTargetIds =
382 AutoTArray<ScrollSnapTargetId, 1>{aEdge.mTargetId};
383 } else {
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;
396 switch (mUnit) {
397 case ScrollUnit::DEVICE_PIXELS:
398 case ScrollUnit::LINES:
399 case ScrollUnit::WHOLE: {
400 isCandidateOfBest =
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));
407 break;
409 case ScrollUnit::PAGES: {
410 // distance to the edge from the scrolling destination in the direction of
411 // scrolling
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) *
417 aScrollingDirection;
419 nscoord secondOvershoot =
420 NSCoordSaturatingSubtract(aCandidateTracker->mSecondBestEdge,
421 aDestination, nscoord_MAX) *
422 aScrollingDirection;
424 // edges between the current position and the scrolling destination are
425 // favoured to preserve context
426 if (overshoot < 0) {
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
433 if (overshoot > 0) {
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.
445 isCandidateOfBest =
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);
471 return true;
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) {
481 return Nothing();
484 if (!aSnapInfo.HasSnapPositions()) {
485 return Nothing();
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
495 // points.
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});
507 break;
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});
517 break;
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) {
532 snapped = true;
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) {
542 snapped = true;
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;
567 Maybe<nscoord> x, y;
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
576 // it.
577 if ((y && !aTarget.mSnapArea.Intersects(
578 nsRect(nsPoint(*aTarget.mSnapPoint.mX, *y),
579 aSnapInfo.mSnapportSize)))) {
580 y.reset();
583 focusedTarget = &aTarget;
584 // If the focused one is valid, then it's the candidate.
585 x = aTarget.mSnapPoint.mX;
588 if (!x) {
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
592 // the
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) {
607 NS_ASSERTION(
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)))) {
618 x.reset();
621 focusedTarget = &aTarget;
622 y = aTarget.mSnapPoint.mY;
625 if (!y) {
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.
636 if (x && y &&
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 ||
640 focusedTarget)) {
641 return false;
643 return true;
646 return {x, y};
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,
658 aCurrentPosition);
661 auto [x, y] = GetCandidateInLastTargets(aSnapInfo, aCurrentPosition,
662 aLastSnapTargetIds, aFocusedContent);
663 if (!x && !y) {
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,
669 aCurrentPosition);
672 // If there's no candidate on one of the axes in the last snap points, try
673 // to find a new candidate.
674 if (!x || !y) {
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);
693 return true;
696 auto finalPos = calcSnapPoints.GetBestEdge(aSnapInfo.mSnapportSize);
697 if (!x) {
698 x = Some(finalPos.mPosition.x);
700 if (!y) {
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
707 // position.
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);
723 return true;
725 return Some(snapTarget);
728 void ScrollSnapUtils::PostPendingResnapIfNeededFor(nsIFrame* aFrame) {
729 ScrollSnapTargetId id = GetTargetIdFor(aFrame);
730 if (id == ScrollSnapTargetId::None) {
731 return;
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
752 // the snapport.
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`
760 // will be conflict.
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