tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ScrollSnap.cpp (35376B)


      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/. */
      6 
      7 #include "ScrollSnap.h"
      8 
      9 #include "FrameMetrics.h"
     10 #include "mozilla/ScrollContainerFrame.h"
     11 #include "mozilla/ScrollSnapInfo.h"
     12 #include "mozilla/ServoStyleConsts.h"
     13 #include "mozilla/StaticPrefs_layout.h"
     14 #include "nsIFrame.h"
     15 #include "nsLayoutUtils.h"
     16 #include "nsPresContext.h"
     17 #include "nsTArray.h"
     18 
     19 namespace mozilla {
     20 
     21 /**
     22 * Keeps track of the current best edge to snap to. The criteria for
     23 * adding an edge depends on the scrolling unit.
     24 */
     25 class CalcSnapPoints final {
     26  using SnapTarget = ScrollSnapInfo::SnapTarget;
     27 
     28 public:
     29  CalcSnapPoints(ScrollUnit aUnit, ScrollSnapFlags aSnapFlags,
     30                 const nsPoint& aDestination, const nsPoint& aStartPos);
     31  struct SnapPosition : public SnapTarget {
     32    SnapPosition(const SnapTarget& aSnapTarget, nscoord aPosition,
     33                 nscoord aDistanceOnOtherAxis)
     34        : SnapTarget(aSnapTarget),
     35          mPosition(aPosition),
     36          mDistanceOnOtherAxis(aDistanceOnOtherAxis) {}
     37 
     38    nscoord mPosition;
     39    // The distance from the scroll destination to this snap position on the
     40    // other axis. This value is used if there are multiple SnapPositions on
     41    // this axis, but the positions on the other axis are different.
     42    nscoord mDistanceOnOtherAxis;
     43  };
     44 
     45  void AddHorizontalEdge(const SnapTarget& aTarget);
     46  void AddVerticalEdge(const SnapTarget& aTarget);
     47 
     48  struct CandidateTracker {
     49    // keeps track of the position of the current second best edge on the
     50    // opposite side of the best edge on this axis.
     51    // We use NSCoordSaturatingSubtract to calculate the distance between a
     52    // given position and this second best edge position so that it can be an
     53    // uninitialized value as the maximum possible value, because the first
     54    // distance calculation would always be nscoord_MAX.
     55    nscoord mSecondBestEdge = nscoord_MAX;
     56 
     57    // Assuming in most cases there's no multiple coincide snap points.
     58    AutoTArray<ScrollSnapTargetId, 1> mTargetIds;
     59    // keeps track of the positions of the current best edge on this axis.
     60    // NOTE: Each SnapPosition.mPosition points the same snap position on this
     61    // axis but other member variables of SnapPosition may have different
     62    // values.
     63    AutoTArray<SnapPosition, 1> mBestEdges;
     64    bool EdgeFound() const { return !mBestEdges.IsEmpty(); }
     65  };
     66  void AddEdge(const SnapPosition& aEdge, nscoord aDestination,
     67               nscoord aStartPos, nscoord aScrollingDirection,
     68               CandidateTracker* aCandidateTracker);
     69  SnapDestination GetBestEdge(const nsSize& aSnapportSize) const;
     70  nscoord XDistanceBetweenBestAndSecondEdge() const {
     71    return std::abs(NSCoordSaturatingSubtract(
     72        mTrackerOnX.mSecondBestEdge,
     73        mTrackerOnX.EdgeFound() ? mTrackerOnX.mBestEdges[0].mPosition
     74                                : mDestination.x,
     75        nscoord_MAX));
     76  }
     77  nscoord YDistanceBetweenBestAndSecondEdge() const {
     78    return std::abs(NSCoordSaturatingSubtract(
     79        mTrackerOnY.mSecondBestEdge,
     80        mTrackerOnY.EdgeFound() ? mTrackerOnY.mBestEdges[0].mPosition
     81                                : mDestination.y,
     82        nscoord_MAX));
     83  }
     84  const nsPoint& Destination() const { return mDestination; }
     85 
     86 protected:
     87  ScrollUnit mUnit;
     88  ScrollSnapFlags mSnapFlags;
     89  nsPoint mDestination;  // gives the position after scrolling but before
     90                         // snapping
     91  nsPoint mStartPos;     // gives the position before scrolling
     92  nsIntPoint mScrollingDirection;  // always -1, 0, or 1
     93  CandidateTracker mTrackerOnX;
     94  CandidateTracker mTrackerOnY;
     95 };
     96 
     97 CalcSnapPoints::CalcSnapPoints(ScrollUnit aUnit, ScrollSnapFlags aSnapFlags,
     98                               const nsPoint& aDestination,
     99                               const nsPoint& aStartPos)
    100    : mUnit(aUnit),
    101      mSnapFlags(aSnapFlags),
    102      mDestination(aDestination),
    103      mStartPos(aStartPos) {
    104  MOZ_ASSERT(aSnapFlags != ScrollSnapFlags::Disabled);
    105 
    106  nsPoint direction = aDestination - aStartPos;
    107  mScrollingDirection = nsIntPoint(0, 0);
    108  if (direction.x < 0) {
    109    mScrollingDirection.x = -1;
    110  }
    111  if (direction.x > 0) {
    112    mScrollingDirection.x = 1;
    113  }
    114  if (direction.y < 0) {
    115    mScrollingDirection.y = -1;
    116  }
    117  if (direction.y > 0) {
    118    mScrollingDirection.y = 1;
    119  }
    120 }
    121 
    122 SnapDestination CalcSnapPoints::GetBestEdge(const nsSize& aSnapportSize) const {
    123  if (mTrackerOnX.EdgeFound() && mTrackerOnY.EdgeFound()) {
    124    nsPoint bestCandidate(mTrackerOnX.mBestEdges[0].mPosition,
    125                          mTrackerOnY.mBestEdges[0].mPosition);
    126    nsRect snappedPort = nsRect(bestCandidate, aSnapportSize);
    127 
    128    // If we've found the candidates on both axes, it's possible some of
    129    // candidates will be outside of the snapport if we snap to the point
    130    // (mTrackerOnX.mBestEdges[0].mPosition,
    131    // mTrackerOnY.mBestEdges[0].mPosition). So we need to get the intersection
    132    // of the snap area of each snap target element on each axis and the
    133    // snapport to tell whether it's outside of the snapport or not.
    134    //
    135    // Also if at least either one of the elements will be outside of the
    136    // snapport if we snap to (mTrackerOnX.mBestEdges[0].mPosition,
    137    // mTrackerOnY.mBestEdges[0].mPosition). We need to choose one of
    138    // combinations of the candidates which is closest to the destination.
    139    //
    140    // So here we iterate over mTrackerOnX and mTrackerOnY just once
    141    // respectively for both purposes to avoid iterating over them again and
    142    // again.
    143    //
    144    // NOTE: Ideally we have to iterate over every possible combinations of
    145    // (mTrackerOnX.mBestEdges[i].mSnapPoint.mY,
    146    //  mTrackerOnY.mBestEdges[j].mSnapPoint.mX) and tell whether the given
    147    // combination will be visible in the snapport or not (maybe we should
    148    // choose the one that the visible area, i.e., the intersection area of
    149    // the snap target elements and the snapport, is the largest one rather than
    150    // the closest one?). But it will be inefficient, so here we will not
    151    // iterate all the combinations, we just iterate all the snap target
    152    // elements in each axis respectively.
    153 
    154    AutoTArray<ScrollSnapTargetId, 1> visibleTargetIdsOnX;
    155    nscoord minimumDistanceOnY = nscoord_MAX;
    156    size_t minimumXIndex = 0;
    157    AutoTArray<ScrollSnapTargetId, 1> minimumDistanceTargetIdsOnX;
    158    for (size_t i = 0; i < mTrackerOnX.mBestEdges.Length(); i++) {
    159      const auto& targetX = mTrackerOnX.mBestEdges[i];
    160      if (targetX.mSnapArea.Intersects(snappedPort)) {
    161        visibleTargetIdsOnX.AppendElement(targetX.mTargetId);
    162      }
    163 
    164      if (targetX.mDistanceOnOtherAxis < minimumDistanceOnY) {
    165        minimumDistanceOnY = targetX.mDistanceOnOtherAxis;
    166        minimumXIndex = i;
    167        minimumDistanceTargetIdsOnX =
    168            AutoTArray<ScrollSnapTargetId, 1>{targetX.mTargetId};
    169      } else if (minimumDistanceOnY != nscoord_MAX &&
    170                 targetX.mDistanceOnOtherAxis == minimumDistanceOnY) {
    171        minimumDistanceTargetIdsOnX.AppendElement(targetX.mTargetId);
    172      }
    173    }
    174 
    175    AutoTArray<ScrollSnapTargetId, 1> visibleTargetIdsOnY;
    176    nscoord minimumDistanceOnX = nscoord_MAX;
    177    size_t minimumYIndex = 0;
    178    AutoTArray<ScrollSnapTargetId, 1> minimumDistanceTargetIdsOnY;
    179    for (size_t i = 0; i < mTrackerOnY.mBestEdges.Length(); i++) {
    180      const auto& targetY = mTrackerOnY.mBestEdges[i];
    181      if (targetY.mSnapArea.Intersects(snappedPort)) {
    182        visibleTargetIdsOnY.AppendElement(targetY.mTargetId);
    183      }
    184 
    185      if (targetY.mDistanceOnOtherAxis < minimumDistanceOnX) {
    186        minimumDistanceOnX = targetY.mDistanceOnOtherAxis;
    187        minimumYIndex = i;
    188        minimumDistanceTargetIdsOnY =
    189            AutoTArray<ScrollSnapTargetId, 1>{targetY.mTargetId};
    190      } else if (minimumDistanceOnX != nscoord_MAX &&
    191                 targetY.mDistanceOnOtherAxis == minimumDistanceOnX) {
    192        minimumDistanceTargetIdsOnY.AppendElement(targetY.mTargetId);
    193      }
    194    }
    195 
    196    // If we have the target ids on both axes, it means the target elements
    197    // (ids) specifying the best edge on X axis and the target elements
    198    // specifying the best edge on Y axis are visible if we snap to the best
    199    // edge. Thus they are valid snap positions.
    200    if (!visibleTargetIdsOnX.IsEmpty() && !visibleTargetIdsOnY.IsEmpty()) {
    201      return SnapDestination{
    202          bestCandidate,
    203          ScrollSnapTargetIds{visibleTargetIdsOnX, visibleTargetIdsOnY}};
    204    }
    205 
    206    // Now we've already known that snapping to
    207    // (mTrackerOnX.mBestEdges[0].mPosition,
    208    // mTrackerOnY.mBestEdges[0].mPosition) will make all candidates of
    209    // mTrackerX or mTrackerY (or both) outside of the snapport. We need to
    210    // choose another combination where candidates of both mTrackerX/Y are
    211    // inside the snapport.
    212 
    213    // There are three possibilities;
    214    // 1) There's no candidate on X axis in mTrackerOnY (that means
    215    //    each candidate's scroll-snap-align is `none` on X axis), but there's
    216    //    any candidate in mTrackerOnX, the closest candidates of mTrackerOnX
    217    //    should be used.
    218    // 2) There's no candidate on Y axis in mTrackerOnX (that means
    219    //    each candidate's scroll-snap-align is `none` on Y axis), but there's
    220    //    any candidate in mTrackerOnY, the closest candidates of mTrackerOnY
    221    //    should be used.
    222    // 3) There are candidates on both axes. Choosing a combination such as
    223    //    (mTrackerOnX.mBestEdges[i].mSnapPoint.mX,
    224    //     mTrackerOnY.mBestEdges[i].mSnapPoint.mY)
    225    //    would require us to iterate over the candidates again if the
    226    //    combination position is outside the snapport, which we don't want to
    227    //    do. Instead, we choose either one of the axis' candidates.
    228    if ((minimumDistanceOnX == nscoord_MAX) &&
    229        minimumDistanceOnY != nscoord_MAX) {
    230      bestCandidate.y = *mTrackerOnX.mBestEdges[minimumXIndex].mSnapPoint.mY;
    231      return SnapDestination{bestCandidate,
    232                             ScrollSnapTargetIds{minimumDistanceTargetIdsOnX,
    233                                                 minimumDistanceTargetIdsOnX}};
    234    }
    235 
    236    if (minimumDistanceOnX != nscoord_MAX &&
    237        minimumDistanceOnY == nscoord_MAX) {
    238      bestCandidate.x = *mTrackerOnY.mBestEdges[minimumYIndex].mSnapPoint.mX;
    239      return SnapDestination{bestCandidate,
    240                             ScrollSnapTargetIds{minimumDistanceTargetIdsOnY,
    241                                                 minimumDistanceTargetIdsOnY}};
    242    }
    243 
    244    if (minimumDistanceOnX != nscoord_MAX &&
    245        minimumDistanceOnY != nscoord_MAX) {
    246      // If we've found candidates on both axes, choose the closest point either
    247      // on X axis or Y axis from the scroll destination. I.e. choose
    248      // `minimumXIndex` one or `minimumYIndex` one to make at least one of
    249      // snap target elements visible inside the snapport.
    250      //
    251      // For example,
    252      // [bestCandidate.x, mTrackerOnX.mBestEdges[minimumXIndex].mSnapPoint.mY]
    253      // is a candidate generated from a single element, thus snapping to the
    254      // point would definitely make the element visible inside the snapport.
    255      if (hypotf(NSCoordToFloat(mDestination.x -
    256                                mTrackerOnX.mBestEdges[0].mPosition),
    257                 NSCoordToFloat(minimumDistanceOnY)) <
    258          hypotf(NSCoordToFloat(minimumDistanceOnX),
    259                 NSCoordToFloat(mDestination.y -
    260                                mTrackerOnY.mBestEdges[0].mPosition))) {
    261        bestCandidate.y = *mTrackerOnX.mBestEdges[minimumXIndex].mSnapPoint.mY;
    262      } else {
    263        bestCandidate.x = *mTrackerOnY.mBestEdges[minimumYIndex].mSnapPoint.mX;
    264      }
    265      return SnapDestination{bestCandidate,
    266                             ScrollSnapTargetIds{minimumDistanceTargetIdsOnX,
    267                                                 minimumDistanceTargetIdsOnY}};
    268    }
    269    MOZ_ASSERT_UNREACHABLE("There's at least one candidate on either axis");
    270    // `minimumDistanceOnX == nscoord_MAX && minimumDistanceOnY == nscoord_MAX`
    271    // should not happen but we fall back for safety.
    272  }
    273 
    274  return SnapDestination{
    275      nsPoint(
    276          mTrackerOnX.EdgeFound() ? mTrackerOnX.mBestEdges[0].mPosition
    277          // In the case of IntendedEndPosition (i.e. the destination point is
    278          // explicitely specied, e.g. scrollTo) use the destination point if we
    279          // didn't find any candidates.
    280          : !(mSnapFlags & ScrollSnapFlags::IntendedDirection) ? mDestination.x
    281                                                               : mStartPos.x,
    282          mTrackerOnY.EdgeFound() ? mTrackerOnY.mBestEdges[0].mPosition
    283          // Same as above X axis case, use the destination point if we didn't
    284          // find any candidates.
    285          : !(mSnapFlags & ScrollSnapFlags::IntendedDirection) ? mDestination.y
    286                                                               : mStartPos.y),
    287      ScrollSnapTargetIds{mTrackerOnX.mTargetIds, mTrackerOnY.mTargetIds}};
    288 }
    289 
    290 void CalcSnapPoints::AddHorizontalEdge(const SnapTarget& aTarget) {
    291  MOZ_ASSERT(aTarget.mSnapPoint.mY);
    292  AddEdge(SnapPosition{aTarget, *aTarget.mSnapPoint.mY,
    293                       aTarget.mSnapPoint.mX
    294                           ? std::abs(mDestination.x - *aTarget.mSnapPoint.mX)
    295                           : nscoord_MAX},
    296          mDestination.y, mStartPos.y, mScrollingDirection.y, &mTrackerOnY);
    297 }
    298 
    299 void CalcSnapPoints::AddVerticalEdge(const SnapTarget& aTarget) {
    300  MOZ_ASSERT(aTarget.mSnapPoint.mX);
    301  AddEdge(SnapPosition{aTarget, *aTarget.mSnapPoint.mX,
    302                       aTarget.mSnapPoint.mY
    303                           ? std::abs(mDestination.y - *aTarget.mSnapPoint.mY)
    304                           : nscoord_MAX},
    305          mDestination.x, mStartPos.x, mScrollingDirection.x, &mTrackerOnX);
    306 }
    307 
    308 void CalcSnapPoints::AddEdge(const SnapPosition& aEdge, nscoord aDestination,
    309                             nscoord aStartPos, nscoord aScrollingDirection,
    310                             CandidateTracker* aCandidateTracker) {
    311  if (mSnapFlags & ScrollSnapFlags::IntendedDirection) {
    312    // In the case of intended direction, we only want to snap to points ahead
    313    // of the direction we are scrolling.
    314    if (aScrollingDirection == 0 ||
    315        (aEdge.mPosition - aStartPos) * aScrollingDirection <= 0) {
    316      // The scroll direction is neutral - will not hit a snap point, or the
    317      // edge is not in the direction we are scrolling, skip it.
    318      return;
    319    }
    320  }
    321 
    322  if (!aCandidateTracker->EdgeFound()) {
    323    aCandidateTracker->mBestEdges = AutoTArray<SnapPosition, 1>{aEdge};
    324    aCandidateTracker->mTargetIds =
    325        AutoTArray<ScrollSnapTargetId, 1>{aEdge.mTargetId};
    326    return;
    327  }
    328 
    329  auto isPreferredStopAlways = [&](const SnapPosition& aSnapPosition) -> bool {
    330    MOZ_ASSERT(mSnapFlags & ScrollSnapFlags::IntendedDirection);
    331    // In the case of intended direction scroll operations, `scroll-snap-stop:
    332    // always` snap points in between the start point and the scroll destination
    333    // are preferable preferable. In other words any `scroll-snap-stop: always`
    334    // snap points can be handled as if it's `scroll-snap-stop: normal`.
    335    return aSnapPosition.mScrollSnapStop == StyleScrollSnapStop::Always &&
    336           std::abs(aSnapPosition.mPosition - aStartPos) <
    337               std::abs(aDestination - aStartPos);
    338  };
    339 
    340  const bool isOnOppositeSide =
    341      ((aEdge.mPosition - aDestination) > 0) !=
    342      ((aCandidateTracker->mBestEdges[0].mPosition - aDestination) > 0);
    343  const nscoord distanceFromStart = aEdge.mPosition - aStartPos;
    344  // A utility function to update the best and the second best edges in the
    345  // given conditions.
    346  // |aIsCloserThanBest| True if the current candidate is closer than the best
    347  // edge.
    348  // |aIsCloserThanSecond| True if the current candidate is closer than
    349  // the second best edge.
    350  const nscoord distanceFromDestination = aEdge.mPosition - aDestination;
    351  auto updateBestEdges = [&](bool aIsCloserThanBest, bool aIsCloserThanSecond) {
    352    if (aIsCloserThanBest) {
    353      if (mSnapFlags & ScrollSnapFlags::IntendedDirection &&
    354          isPreferredStopAlways(aEdge)) {
    355        // In the case of intended direction scroll operations and the new best
    356        // candidate is `scroll-snap-stop: always` and if it's closer to the
    357        // start position than the destination, thus we won't use the second
    358        // best edge since even if the snap port of the best edge covers entire
    359        // snapport, the `scroll-snap-stop: always` snap point is preferred than
    360        // any points.
    361        // NOTE: We've already ignored snap points behind start points so that
    362        // we can use std::abs here in the comparison.
    363        //
    364        // For example, if there's a `scroll-snap-stop: always` in between the
    365        // start point and destination, no `snap-overflow` mechanism should
    366        // happen, if there's `scroll-snap-stop: always` further than the
    367        // destination, `snap-overflow` might happen something like below
    368        // diagram.
    369        // start        always    dest   other always
    370        //   |------------|---------|------|
    371        aCandidateTracker->mSecondBestEdge = aEdge.mPosition;
    372      } else if (isOnOppositeSide) {
    373        // Replace the second best edge with the current best edge only if the
    374        // new best edge (aEdge) is on the opposite side of the current best
    375        // edge.
    376        aCandidateTracker->mSecondBestEdge =
    377            aCandidateTracker->mBestEdges[0].mPosition;
    378      }
    379      aCandidateTracker->mBestEdges = AutoTArray<SnapPosition, 1>{aEdge};
    380      aCandidateTracker->mTargetIds =
    381          AutoTArray<ScrollSnapTargetId, 1>{aEdge.mTargetId};
    382    } else {
    383      if (aEdge.mPosition == aCandidateTracker->mBestEdges[0].mPosition) {
    384        aCandidateTracker->mTargetIds.AppendElement(aEdge.mTargetId);
    385        aCandidateTracker->mBestEdges.AppendElement(aEdge);
    386      }
    387      if (aIsCloserThanSecond && isOnOppositeSide) {
    388        aCandidateTracker->mSecondBestEdge = aEdge.mPosition;
    389      }
    390    }
    391  };
    392 
    393  bool isCandidateOfBest = false;
    394  bool isCandidateOfSecondBest = false;
    395  switch (mUnit) {
    396    case ScrollUnit::DEVICE_PIXELS:
    397    case ScrollUnit::LINES:
    398    case ScrollUnit::WHOLE: {
    399      isCandidateOfBest =
    400          std::abs(distanceFromDestination) <
    401          std::abs(aCandidateTracker->mBestEdges[0].mPosition - aDestination);
    402      isCandidateOfSecondBest =
    403          std::abs(distanceFromDestination) <
    404          std::abs(NSCoordSaturatingSubtract(aCandidateTracker->mSecondBestEdge,
    405                                             aDestination, nscoord_MAX));
    406      break;
    407    }
    408    case ScrollUnit::PAGES: {
    409      // distance to the edge from the scrolling destination in the direction of
    410      // scrolling
    411      nscoord overshoot = distanceFromDestination * aScrollingDirection;
    412      // distance to the current best edge from the scrolling destination in the
    413      // direction of scrolling
    414      nscoord curOvershoot =
    415          (aCandidateTracker->mBestEdges[0].mPosition - aDestination) *
    416          aScrollingDirection;
    417 
    418      nscoord secondOvershoot =
    419          NSCoordSaturatingSubtract(aCandidateTracker->mSecondBestEdge,
    420                                    aDestination, nscoord_MAX) *
    421          aScrollingDirection;
    422 
    423      // edges between the current position and the scrolling destination are
    424      // favoured to preserve context
    425      if (overshoot < 0) {
    426        isCandidateOfBest = overshoot > curOvershoot || curOvershoot >= 0;
    427        isCandidateOfSecondBest =
    428            overshoot > secondOvershoot || secondOvershoot >= 0;
    429      }
    430      // if there are no edges between the current position and the scrolling
    431      // destination the closest edge beyond the destination is used
    432      if (overshoot > 0) {
    433        isCandidateOfBest = overshoot < curOvershoot;
    434        isCandidateOfSecondBest = overshoot < secondOvershoot;
    435      }
    436    }
    437  }
    438 
    439  if (mSnapFlags & ScrollSnapFlags::IntendedDirection) {
    440    if (isPreferredStopAlways(aEdge)) {
    441      // If the given position is `scroll-snap-stop: always` and if the position
    442      // is in between the start and the destination positions, update the best
    443      // position based on the distance from the __start__ point.
    444      isCandidateOfBest =
    445          std::abs(distanceFromStart) <
    446          std::abs(aCandidateTracker->mBestEdges[0].mPosition - aStartPos);
    447    } else if (isPreferredStopAlways(aCandidateTracker->mBestEdges[0])) {
    448      // If we've found a preferable `scroll-snap-stop:always` position as the
    449      // best, do not update it unless the given position is also
    450      // `scroll-snap-stop: always`.
    451      isCandidateOfBest = false;
    452    }
    453  }
    454 
    455  updateBestEdges(isCandidateOfBest, isCandidateOfSecondBest);
    456 }
    457 
    458 static void ProcessSnapPositions(CalcSnapPoints& aCalcSnapPoints,
    459                                 const ScrollSnapInfo& aSnapInfo) {
    460  aSnapInfo.ForEachValidTargetFor(
    461      aCalcSnapPoints.Destination(), [&](const auto& aTarget) -> bool {
    462        if (aTarget.mSnapPoint.mX && aSnapInfo.mScrollSnapStrictnessX !=
    463                                         StyleScrollSnapStrictness::None) {
    464          aCalcSnapPoints.AddVerticalEdge(aTarget);
    465        }
    466        if (aTarget.mSnapPoint.mY && aSnapInfo.mScrollSnapStrictnessY !=
    467                                         StyleScrollSnapStrictness::None) {
    468          aCalcSnapPoints.AddHorizontalEdge(aTarget);
    469        }
    470        return true;
    471      });
    472 }
    473 
    474 Maybe<SnapDestination> ScrollSnapUtils::GetSnapPointForDestination(
    475    const ScrollSnapInfo& aSnapInfo, ScrollUnit aUnit,
    476    ScrollSnapFlags aSnapFlags, const nsRect& aScrollRange,
    477    const nsPoint& aStartPos, const nsPoint& aDestination) {
    478  if (aSnapInfo.mScrollSnapStrictnessY == StyleScrollSnapStrictness::None &&
    479      aSnapInfo.mScrollSnapStrictnessX == StyleScrollSnapStrictness::None) {
    480    return Nothing();
    481  }
    482 
    483  if (!aSnapInfo.HasSnapPositions()) {
    484    return Nothing();
    485  }
    486 
    487  CalcSnapPoints calcSnapPoints(aUnit, aSnapFlags, aDestination, aStartPos);
    488 
    489  ProcessSnapPositions(calcSnapPoints, aSnapInfo);
    490 
    491  // If the distance between the first and the second candidate snap points
    492  // is larger than the snapport size and the snapport is covered by larger
    493  // elements, any points inside the covering area should be valid snap
    494  // points.
    495  // https://drafts.csswg.org/css-scroll-snap-1/#snap-overflow
    496  // NOTE: |aDestination| sometimes points outside of the scroll range, e.g.
    497  // by the APZC fling, so for the overflow checks we need to clamp it.
    498  nsPoint clampedDestination = aScrollRange.ClampPoint(aDestination);
    499  for (auto range : aSnapInfo.mXRangeWiderThanSnapport) {
    500    if (range.IsValid(clampedDestination.x, aSnapInfo.mSnapportSize.width) &&
    501        calcSnapPoints.XDistanceBetweenBestAndSecondEdge() >
    502            aSnapInfo.mSnapportSize.width) {
    503      calcSnapPoints.AddVerticalEdge(ScrollSnapInfo::SnapTarget{
    504          Some(clampedDestination.x), Nothing(), range.mSnapArea,
    505          StyleScrollSnapStop::Normal, ScrollSnapTargetId::None});
    506      break;
    507    }
    508  }
    509  for (auto range : aSnapInfo.mYRangeWiderThanSnapport) {
    510    if (range.IsValid(clampedDestination.y, aSnapInfo.mSnapportSize.height) &&
    511        calcSnapPoints.YDistanceBetweenBestAndSecondEdge() >
    512            aSnapInfo.mSnapportSize.height) {
    513      calcSnapPoints.AddHorizontalEdge(ScrollSnapInfo::SnapTarget{
    514          Nothing(), Some(clampedDestination.y), range.mSnapArea,
    515          StyleScrollSnapStop::Normal, ScrollSnapTargetId::None});
    516      break;
    517    }
    518  }
    519 
    520  bool snapped = false;
    521  auto finalPos = calcSnapPoints.GetBestEdge(aSnapInfo.mSnapportSize);
    522 
    523  // Check whether we will snap to the final position on the given axis or not,
    524  // and if we will not, reset the final position to the original position so
    525  // that even if we need to snap on an axis, but we don't need to on the other
    526  // axis, the returned final position can be used as a valid destination.
    527  auto checkSnapOnAxis = [&snapped](StyleScrollSnapStrictness aStrictness,
    528                                    nscoord aDestination, nscoord aSnapportSize,
    529                                    nscoord& aFinalPosition) {
    530    // We used 0.3 proximity threshold which is what WebKit uses.
    531    constexpr float proximityRatio = 0.3;
    532    if (aStrictness == StyleScrollSnapStrictness::None ||
    533        (aStrictness == StyleScrollSnapStrictness::Proximity &&
    534         std::abs(aDestination - aFinalPosition) >
    535             aSnapportSize * proximityRatio)) {
    536      aFinalPosition = aDestination;
    537      return;
    538    }
    539    snapped = true;
    540  };
    541 
    542  checkSnapOnAxis(aSnapInfo.mScrollSnapStrictnessY, aDestination.y,
    543                  aSnapInfo.mSnapportSize.height, finalPos.mPosition.y);
    544  checkSnapOnAxis(aSnapInfo.mScrollSnapStrictnessX, aDestination.x,
    545                  aSnapInfo.mSnapportSize.width, finalPos.mPosition.x);
    546 
    547  return snapped ? Some(finalPos) : Nothing();
    548 }
    549 
    550 ScrollSnapTargetId ScrollSnapUtils::GetTargetIdFor(const nsIFrame* aFrame) {
    551  MOZ_ASSERT(aFrame && aFrame->GetContent());
    552  return ScrollSnapTargetId{reinterpret_cast<uintptr_t>(aFrame->GetContent())};
    553 }
    554 
    555 static std::pair<Maybe<nscoord>, Maybe<nscoord>> GetCandidateInLastTargets(
    556    const ScrollSnapInfo& aSnapInfo, const nsPoint& aCurrentPosition,
    557    const UniquePtr<ScrollSnapTargetIds>& aLastSnapTargetIds,
    558    const nsIContent* aFocusedContent) {
    559  ScrollSnapTargetId targetIdForFocusedContent = ScrollSnapTargetId::None;
    560  if (aFocusedContent && aFocusedContent->GetPrimaryFrame()) {
    561    targetIdForFocusedContent =
    562        ScrollSnapUtils::GetTargetIdFor(aFocusedContent->GetPrimaryFrame());
    563  }
    564 
    565  // Note: Below algorithm doesn't care about cases where the last snap point
    566  // was on an element larger than the snapport since it's not clear to us
    567  // what we should do for now.
    568  // https://github.com/w3c/csswg-drafts/issues/7438
    569  const ScrollSnapInfo::SnapTarget* focusedTarget = nullptr;
    570  Maybe<nscoord> x, y;
    571  aSnapInfo.ForEachValidTargetFor(
    572      aCurrentPosition, [&](const auto& aTarget) -> bool {
    573        if (aTarget.mSnapPoint.mX && aSnapInfo.mScrollSnapStrictnessX !=
    574                                         StyleScrollSnapStrictness::None) {
    575          if (aLastSnapTargetIds->mIdsOnX.Contains(aTarget.mTargetId)) {
    576            if (targetIdForFocusedContent == aTarget.mTargetId) {
    577              // If we've already found the candidate on Y axis, but if snapping
    578              // to the point results this target is scrolled out, we can't use
    579              // it.
    580              if ((y && !aTarget.mSnapArea.Intersects(
    581                            nsRect(nsPoint(*aTarget.mSnapPoint.mX, *y),
    582                                   aSnapInfo.mSnapportSize)))) {
    583                y.reset();
    584              }
    585 
    586              focusedTarget = &aTarget;
    587              // If the focused one is valid, then it's the candidate.
    588              x = aTarget.mSnapPoint.mX;
    589            }
    590 
    591            if (!x) {
    592              // Update the candidate on X axis only if
    593              // 1) we haven't yet found the candidate on Y axis
    594              // 2) or if we've found the candiate on Y axis and if snapping to
    595              // the
    596              //    candidate position result the target element is visible
    597              //    inside the snapport.
    598              if (!y || (y && aTarget.mSnapArea.Intersects(
    599                                  nsRect(nsPoint(*aTarget.mSnapPoint.mX, *y),
    600                                         aSnapInfo.mSnapportSize)))) {
    601                x = aTarget.mSnapPoint.mX;
    602              }
    603            }
    604          }
    605        }
    606        if (aTarget.mSnapPoint.mY && aSnapInfo.mScrollSnapStrictnessY !=
    607                                         StyleScrollSnapStrictness::None) {
    608          if (aLastSnapTargetIds->mIdsOnY.Contains(aTarget.mTargetId)) {
    609            if (targetIdForFocusedContent == aTarget.mTargetId) {
    610              NS_ASSERTION(
    611                  !focusedTarget || focusedTarget == &aTarget,
    612                  "If the focused target has been found on X axis, the "
    613                  "target should be same");
    614              // If we've already found the candidate on X axis other than the
    615              // focused one, but if snapping to the point results this target
    616              // is scrolled out, we can't use it.
    617              if (!focusedTarget &&
    618                  (x && !aTarget.mSnapArea.Intersects(
    619                            nsRect(nsPoint(*x, *aTarget.mSnapPoint.mY),
    620                                   aSnapInfo.mSnapportSize)))) {
    621                x.reset();
    622              }
    623 
    624              focusedTarget = &aTarget;
    625              y = aTarget.mSnapPoint.mY;
    626            }
    627 
    628            if (!y) {
    629              if (!x || (x && aTarget.mSnapArea.Intersects(
    630                                  nsRect(nsPoint(*x, *aTarget.mSnapPoint.mY),
    631                                         aSnapInfo.mSnapportSize)))) {
    632                y = aTarget.mSnapPoint.mY;
    633              }
    634            }
    635          }
    636        }
    637 
    638        // If we found candidates on both axes, it's the one we need.
    639        if (x && y &&
    640            // If we haven't found the focused target, it's possible that we
    641            // haven't iterated it, don't break in such case.
    642            (targetIdForFocusedContent == ScrollSnapTargetId::None ||
    643             focusedTarget)) {
    644          return false;
    645        }
    646        return true;
    647      });
    648 
    649  return {x, y};
    650 }
    651 
    652 Maybe<SnapDestination> ScrollSnapUtils::GetSnapPointForResnap(
    653    const ScrollSnapInfo& aSnapInfo, const nsRect& aScrollRange,
    654    const nsPoint& aCurrentPosition,
    655    const UniquePtr<ScrollSnapTargetIds>& aLastSnapTargetIds,
    656    const nsIContent* aFocusedContent) {
    657  if (!aLastSnapTargetIds) {
    658    return GetSnapPointForDestination(aSnapInfo, ScrollUnit::DEVICE_PIXELS,
    659                                      ScrollSnapFlags::IntendedEndPosition,
    660                                      aScrollRange, aCurrentPosition,
    661                                      aCurrentPosition);
    662  }
    663 
    664  auto [x, y] = GetCandidateInLastTargets(aSnapInfo, aCurrentPosition,
    665                                          aLastSnapTargetIds, aFocusedContent);
    666  if (!x && !y) {
    667    // In the worst case there's no longer valid snap points previously snapped,
    668    // try to find new valid snap points.
    669    return GetSnapPointForDestination(aSnapInfo, ScrollUnit::DEVICE_PIXELS,
    670                                      ScrollSnapFlags::IntendedEndPosition,
    671                                      aScrollRange, aCurrentPosition,
    672                                      aCurrentPosition);
    673  }
    674 
    675  // If there's no candidate on one of the axes in the last snap points, try
    676  // to find a new candidate.
    677  if (!x || !y) {
    678    nsPoint newPosition =
    679        nsPoint(x ? *x : aCurrentPosition.x, y ? *y : aCurrentPosition.y);
    680    CalcSnapPoints calcSnapPoints(ScrollUnit::DEVICE_PIXELS,
    681                                  ScrollSnapFlags::IntendedEndPosition,
    682                                  newPosition, newPosition);
    683 
    684    aSnapInfo.ForEachValidTargetFor(
    685        newPosition, [&, &x = x, &y = y](const auto& aTarget) -> bool {
    686          if (!x && aTarget.mSnapPoint.mX &&
    687              aSnapInfo.mScrollSnapStrictnessX !=
    688                  StyleScrollSnapStrictness::None) {
    689            calcSnapPoints.AddVerticalEdge(aTarget);
    690          }
    691          if (!y && aTarget.mSnapPoint.mY &&
    692              aSnapInfo.mScrollSnapStrictnessY !=
    693                  StyleScrollSnapStrictness::None) {
    694            calcSnapPoints.AddHorizontalEdge(aTarget);
    695          }
    696          return true;
    697        });
    698 
    699    auto finalPos = calcSnapPoints.GetBestEdge(aSnapInfo.mSnapportSize);
    700    if (!x) {
    701      x = Some(finalPos.mPosition.x);
    702    }
    703    if (!y) {
    704      y = Some(finalPos.mPosition.y);
    705    }
    706  }
    707 
    708  SnapDestination snapTarget{nsPoint(*x, *y)};
    709  // Collect snap points where the position is still same as the new snap
    710  // position.
    711  aSnapInfo.ForEachValidTargetFor(
    712      snapTarget.mPosition, [&, &x = x, &y = y](const auto& aTarget) -> bool {
    713        if (aTarget.mSnapPoint.mX &&
    714            aSnapInfo.mScrollSnapStrictnessX !=
    715                StyleScrollSnapStrictness::None &&
    716            aTarget.mSnapPoint.mX == x) {
    717          snapTarget.mTargetIds.mIdsOnX.AppendElement(aTarget.mTargetId);
    718        }
    719 
    720        if (aTarget.mSnapPoint.mY &&
    721            aSnapInfo.mScrollSnapStrictnessY !=
    722                StyleScrollSnapStrictness::None &&
    723            aTarget.mSnapPoint.mY == y) {
    724          snapTarget.mTargetIds.mIdsOnY.AppendElement(aTarget.mTargetId);
    725        }
    726        return true;
    727      });
    728  return Some(snapTarget);
    729 }
    730 
    731 void ScrollSnapUtils::PostPendingResnapIfNeededFor(nsIFrame* aFrame) {
    732  ScrollSnapTargetId id = GetTargetIdFor(aFrame);
    733  if (id == ScrollSnapTargetId::None) {
    734    return;
    735  }
    736 
    737  if (ScrollContainerFrame* sf = nsLayoutUtils::GetNearestScrollContainerFrame(
    738          aFrame, nsLayoutUtils::SCROLLABLE_SAME_DOC |
    739                      nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN)) {
    740    sf->PostPendingResnapIfNeeded(aFrame);
    741  }
    742 }
    743 
    744 void ScrollSnapUtils::PostPendingResnapFor(nsIFrame* aFrame) {
    745  if (ScrollContainerFrame* sf = nsLayoutUtils::GetNearestScrollContainerFrame(
    746          aFrame, nsLayoutUtils::SCROLLABLE_SAME_DOC |
    747                      nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN)) {
    748    sf->PostPendingResnap();
    749  }
    750 }
    751 
    752 bool ScrollSnapUtils::NeedsToRespectTargetWritingMode(
    753    const nsSize& aSnapAreaSize, const nsSize& aSnapportSize) {
    754  // Use the writing-mode on the target element if the snap area is larger than
    755  // the snapport.
    756  // https://drafts.csswg.org/css-scroll-snap/#snap-scope
    757  //
    758  // It's unclear `larger` means that the size is larger than only on the target
    759  // axis. If it doesn't, it will pick the same axis in the case where only one
    760  // axis is larger. For example, if an element size is (200 x 10) and the
    761  // snapport size is (100 x 100) and if the element's writing mode is different
    762  // from the scroller's writing mode, then `scroll-snap-align: start start`
    763  // will be conflict.
    764  return aSnapAreaSize.width > aSnapportSize.width ||
    765         aSnapAreaSize.height > aSnapportSize.height;
    766 }
    767 
    768 static nsRect InflateByScrollMargin(const nsRect& aTargetRect,
    769                                    const nsMargin& aScrollMargin,
    770                                    const nsRect& aScrolledRect) {
    771  // Inflate the rect by scroll-margin.
    772  nsRect result = aTargetRect;
    773  result.Inflate(aScrollMargin);
    774 
    775  // But don't be beyond the limit boundary.
    776  return result.Intersect(aScrolledRect);
    777 }
    778 
    779 nsRect ScrollSnapUtils::GetSnapAreaFor(const nsIFrame* aFrame,
    780                                       const nsIFrame* aScrolledFrame,
    781                                       const nsRect& aScrolledRect) {
    782  nsRect targetRect = nsLayoutUtils::TransformFrameRectToAncestor(
    783      aFrame, aFrame->GetRectRelativeToSelf(), aScrolledFrame);
    784 
    785  // The snap area contains scroll-margin values.
    786  // https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-area
    787  nsMargin scrollMargin = aFrame->StyleMargin()->GetScrollMargin();
    788  return InflateByScrollMargin(targetRect, scrollMargin, aScrolledRect);
    789 }
    790 
    791 }  // namespace mozilla