tor-browser

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

ScrollAnchorContainer.cpp (30275B)


      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 "ScrollAnchorContainer.h"
      8 
      9 #include <cstddef>
     10 
     11 #include "mozilla/PresShell.h"
     12 #include "mozilla/ProfilerLabels.h"
     13 #include "mozilla/ScopeExit.h"
     14 #include "mozilla/ScrollContainerFrame.h"
     15 #include "mozilla/StaticPrefs_layout.h"
     16 #include "mozilla/ToString.h"
     17 #include "mozilla/dom/Text.h"
     18 #include "nsBlockFrame.h"
     19 #include "nsIFrame.h"
     20 #include "nsIFrameInlines.h"
     21 #include "nsLayoutUtils.h"
     22 #include "nsPlaceholderFrame.h"
     23 
     24 using namespace mozilla::dom;
     25 
     26 #ifdef DEBUG
     27 static mozilla::LazyLogModule sAnchorLog("scrollanchor");
     28 
     29 #  define ANCHOR_LOG_WITH(anchor_, fmt, ...)              \
     30    MOZ_LOG(sAnchorLog, LogLevel::Debug,                  \
     31            ("ANCHOR(%p, %s, root: %d): " fmt, (anchor_), \
     32             (anchor_)                                    \
     33                 ->Frame()                                \
     34                 ->PresContext()                          \
     35                 ->Document()                             \
     36                 ->GetDocumentURI()                       \
     37                 ->GetSpecOrDefault()                     \
     38                 .get(),                                  \
     39             (anchor_)->Frame()->mIsRoot, ##__VA_ARGS__));
     40 
     41 #  define ANCHOR_LOG(fmt, ...) ANCHOR_LOG_WITH(this, fmt, ##__VA_ARGS__)
     42 #else
     43 #  define ANCHOR_LOG(...)
     44 #  define ANCHOR_LOG_WITH(...)
     45 #endif
     46 
     47 namespace mozilla::layout {
     48 
     49 ScrollContainerFrame* ScrollAnchorContainer::Frame() const {
     50  return reinterpret_cast<ScrollContainerFrame*>(
     51      ((char*)this) - offsetof(ScrollContainerFrame, mAnchor));
     52 }
     53 
     54 ScrollAnchorContainer::ScrollAnchorContainer(ScrollContainerFrame* aScrollFrame)
     55    : mDisabled(false),
     56      mAnchorMightBeSubOptimal(false),
     57      mAnchorNodeIsDirty(true),
     58      mApplyingAnchorAdjustment(false),
     59      mSuppressAnchorAdjustment(false) {
     60  MOZ_ASSERT(aScrollFrame == Frame());
     61 }
     62 
     63 ScrollAnchorContainer::~ScrollAnchorContainer() = default;
     64 
     65 ScrollAnchorContainer* ScrollAnchorContainer::FindFor(nsIFrame* aFrame) {
     66  aFrame = aFrame->GetParent();
     67  if (!aFrame) {
     68    return nullptr;
     69  }
     70  ScrollContainerFrame* nearest = nsLayoutUtils::GetNearestScrollContainerFrame(
     71      aFrame, nsLayoutUtils::SCROLLABLE_SAME_DOC |
     72                  nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
     73  if (nearest) {
     74    return nearest->Anchor();
     75  }
     76  return nullptr;
     77 }
     78 
     79 ScrollContainerFrame* ScrollAnchorContainer::ScrollContainer() const {
     80  return Frame()->GetScrollTargetFrame();
     81 }
     82 
     83 /**
     84 * Set the appropriate frame flags for a frame that has become or is no longer
     85 * an anchor node.
     86 */
     87 static void SetAnchorFlags(const nsIFrame* aScrolledFrame,
     88                           nsIFrame* aAnchorNode, bool aInScrollAnchorChain) {
     89  nsIFrame* frame = aAnchorNode;
     90  while (frame && frame != aScrolledFrame) {
     91    // TODO(emilio, bug 1629280): This commented out assertion below should
     92    // hold, but it may not in the case of reparenting-during-reflow (due to
     93    // inline fragmentation or such). That looks fishy!
     94    //
     95    // We should either invalidate the anchor when reparenting any frame on the
     96    // chain, or fix up the chain flags.
     97    //
     98    // MOZ_DIAGNOSTIC_ASSERT(frame->IsInScrollAnchorChain() !=
     99    //                       aInScrollAnchorChain);
    100    frame->SetInScrollAnchorChain(aInScrollAnchorChain);
    101    frame = frame->GetParent();
    102  }
    103  MOZ_ASSERT(frame,
    104             "The anchor node should be a descendant of the scrolled frame");
    105  // If needed, invalidate the frame so that we start/stop highlighting the
    106  // anchor
    107  if (StaticPrefs::layout_css_scroll_anchoring_highlight()) {
    108    for (nsIFrame* frame = aAnchorNode->FirstContinuation(); !!frame;
    109         frame = frame->GetNextContinuation()) {
    110      frame->InvalidateFrame();
    111    }
    112  }
    113 }
    114 
    115 /**
    116 * Compute the scrollable overflow rect [1] of aCandidate relative to
    117 * aScrollFrame with all transforms applied.
    118 *
    119 * The specification is ambiguous about what can be selected as a scroll anchor,
    120 * which makes the scroll anchoring bounding rect partially undefined [2]. This
    121 * code attempts to match the implementation in Blink.
    122 *
    123 * An additional unspecified behavior is that any scrollable overflow before the
    124 * border start edge in the block axis of aScrollFrame should be clamped. This
    125 * is to prevent absolutely positioned descendant elements from being able to
    126 * trigger scroll adjustments [3].
    127 *
    128 * [1]
    129 * https://drafts.csswg.org/css-scroll-anchoring-1/#scroll-anchoring-bounding-rect
    130 * [2] https://github.com/w3c/csswg-drafts/issues/3478
    131 * [3] https://bugzilla.mozilla.org/show_bug.cgi?id=1519541
    132 */
    133 static nsRect FindScrollAnchoringBoundingRect(const nsIFrame* aScrollFrame,
    134                                              nsIFrame* aCandidate) {
    135  MOZ_ASSERT(nsLayoutUtils::IsProperAncestorFrame(aScrollFrame, aCandidate));
    136  if (!!Text::FromNodeOrNull(aCandidate->GetContent())) {
    137    // This is a frame for a text node. The spec says we need to accumulate the
    138    // union of all line boxes in the coordinate space of the scroll frame
    139    // accounting for transforms.
    140    //
    141    // To do this, we translate and accumulate the overflow rect for each text
    142    // continuation to the coordinate space of the nearest ancestor block
    143    // frame. Then we transform the resulting rect into the coordinate space of
    144    // the scroll frame.
    145    //
    146    // Transforms aren't allowed on non-replaced inline boxes, so we can assume
    147    // that these text node continuations will have the same transform as their
    148    // nearest block ancestor. And it should be faster to transform their union
    149    // rather than individually transforming each overflow rect
    150    //
    151    // XXX for fragmented blocks, blockAncestor will be an ancestor only to the
    152    //     text continuations in the first block continuation. GetOffsetTo
    153    //     should continue to work, but is it correct with transforms or a
    154    //     performance hazard?
    155    nsIFrame* blockAncestor =
    156        nsLayoutUtils::FindNearestBlockAncestor(aCandidate);
    157    MOZ_ASSERT(
    158        nsLayoutUtils::IsProperAncestorFrame(aScrollFrame, blockAncestor));
    159    nsRect bounding;
    160    for (nsIFrame* continuation = aCandidate->FirstContinuation(); continuation;
    161         continuation = continuation->GetNextContinuation()) {
    162      nsRect overflowRect =
    163          continuation->ScrollableOverflowRectRelativeToSelf();
    164      overflowRect += continuation->GetOffsetTo(blockAncestor);
    165      bounding = bounding.Union(overflowRect);
    166    }
    167    return nsLayoutUtils::TransformFrameRectToAncestor(blockAncestor, bounding,
    168                                                       aScrollFrame);
    169  }
    170 
    171  nsRect borderRect = aCandidate->GetRectRelativeToSelf();
    172  nsRect overflowRect = aCandidate->ScrollableOverflowRectRelativeToSelf();
    173 
    174  NS_ASSERTION(overflowRect.Contains(borderRect),
    175               "overflow rect must include border rect, and the clamping logic "
    176               "here depends on that");
    177 
    178  // Clamp the scrollable overflow rect to the border start edge on the block
    179  // axis of the scroll frame
    180  WritingMode writingMode = aScrollFrame->GetWritingMode();
    181  switch (writingMode.GetBlockDir()) {
    182    case WritingMode::BlockDir::TB: {
    183      overflowRect.SetBoxY(borderRect.Y(), overflowRect.YMost());
    184      break;
    185    }
    186    case WritingMode::BlockDir::LR: {
    187      overflowRect.SetBoxX(borderRect.X(), overflowRect.XMost());
    188      break;
    189    }
    190    case WritingMode::BlockDir::RL: {
    191      overflowRect.SetBoxX(overflowRect.X(), borderRect.XMost());
    192      break;
    193    }
    194  }
    195 
    196  nsRect transformed = nsLayoutUtils::TransformFrameRectToAncestor(
    197      aCandidate, overflowRect, aScrollFrame);
    198  return transformed;
    199 }
    200 
    201 /**
    202 * Compute the offset between the scrollable overflow rect start edge of
    203 * aCandidate and the scroll-port start edge of aScrollContainerFrame, in the
    204 * block axis of aScrollContainerFrame.
    205 */
    206 static nscoord FindScrollAnchoringBoundingOffset(
    207    const ScrollContainerFrame* aScrollContainerFrame, nsIFrame* aCandidate) {
    208  WritingMode writingMode = aScrollContainerFrame->GetWritingMode();
    209  nsRect physicalBounding =
    210      FindScrollAnchoringBoundingRect(aScrollContainerFrame, aCandidate);
    211  LogicalRect logicalBounding(
    212      writingMode, physicalBounding,
    213      aScrollContainerFrame->GetScrolledFrame()->GetSize());
    214  return logicalBounding.BStart(writingMode);
    215 }
    216 
    217 bool ScrollAnchorContainer::CanMaintainAnchor() const {
    218  if (!StaticPrefs::layout_css_scroll_anchoring_enabled()) {
    219    return false;
    220  }
    221 
    222  // If we've been disabled due to heuristics, we don't anchor anymore.
    223  if (mDisabled) {
    224    return false;
    225  }
    226 
    227  const nsStyleDisplay& disp = *Frame()->StyleDisplay();
    228  // Don't select a scroll anchor if the scroll frame has `overflow-anchor:
    229  // none`.
    230  if (disp.mOverflowAnchor != mozilla::StyleOverflowAnchor::Auto) {
    231    return false;
    232  }
    233 
    234  // Or if the scroll frame has not been scrolled from the logical origin of the
    235  // block axis. This is not in the specification [1], but Blink does this [2].
    236  //
    237  // [1] https://github.com/w3c/csswg-drafts/issues/3319
    238  // [2]
    239  // https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/layout/scroll_anchor.cc;l=551;drc=f1eab630d343484302ee9bea91f515f1a1dd0891
    240  const nsPoint pos = Frame()->GetLogicalScrollPosition();
    241  const nscoord blockOffset =
    242      Frame()->GetWritingMode().IsVertical() ? pos.x : pos.y;
    243  if (blockOffset == 0) {
    244    return false;
    245  }
    246 
    247  // Or if there is perspective that could affect the scrollable overflow rect
    248  // for descendant frames. This is not in the specification as Blink doesn't
    249  // share this behavior with perspective [1].
    250  //
    251  // [1] https://github.com/w3c/csswg-drafts/issues/3322
    252  if (Frame()->ChildrenHavePerspective()) {
    253    return false;
    254  }
    255 
    256  return true;
    257 }
    258 
    259 void ScrollAnchorContainer::SelectAnchor() {
    260  MOZ_ASSERT(Frame()->mScrolledFrame);
    261  MOZ_ASSERT(mAnchorNodeIsDirty);
    262 
    263  AUTO_PROFILER_LABEL("ScrollAnchorContainer::SelectAnchor", LAYOUT);
    264  ANCHOR_LOG("Selecting anchor with scroll-port=%s.\n",
    265             mozilla::ToString(Frame()->GetVisualOptimalViewingRect()).c_str());
    266 
    267  // Select a new scroll anchor
    268  nsIFrame* oldAnchor = mAnchorNode;
    269  if (CanMaintainAnchor()) {
    270    MOZ_DIAGNOSTIC_ASSERT(
    271        !Frame()->mScrolledFrame->IsInScrollAnchorChain(),
    272        "Our scrolled frame can't serve as or contain an anchor for an "
    273        "ancestor if it can maintain its own anchor");
    274    ANCHOR_LOG("Beginning selection.\n");
    275    mAnchorNode = FindAnchorIn(Frame()->mScrolledFrame);
    276  } else {
    277    ANCHOR_LOG("Skipping selection, doesn't maintain a scroll anchor.\n");
    278    mAnchorNode = nullptr;
    279  }
    280  mAnchorMightBeSubOptimal =
    281      mAnchorNode && mAnchorNode->HasAnyStateBits(NS_FRAME_HAS_DIRTY_CHILDREN);
    282 
    283  // Update the anchor flags if needed
    284  if (oldAnchor != mAnchorNode) {
    285    ANCHOR_LOG("Anchor node has changed from (%p) to (%p).\n", oldAnchor,
    286               mAnchorNode);
    287 
    288    // Unset all flags for the old scroll anchor
    289    if (oldAnchor) {
    290      SetAnchorFlags(Frame()->mScrolledFrame, oldAnchor, false);
    291    }
    292 
    293    // Set all flags for the new scroll anchor
    294    if (mAnchorNode) {
    295      // Anchor selection will never select a descendant of a nested scroll
    296      // frame which maintains an anchor, so we can set flags without
    297      // conflicting with other scroll anchor containers.
    298      SetAnchorFlags(Frame()->mScrolledFrame, mAnchorNode, true);
    299    }
    300  } else {
    301    ANCHOR_LOG("Anchor node has remained (%p).\n", mAnchorNode);
    302  }
    303 
    304  // Calculate the position to use for scroll adjustments
    305  if (mAnchorNode) {
    306    mLastAnchorOffset = FindScrollAnchoringBoundingOffset(Frame(), mAnchorNode);
    307    ANCHOR_LOG("Using last anchor offset = %s.\n",
    308               ToString(CSSPixel::FromAppUnits(mLastAnchorOffset)).c_str());
    309  } else {
    310    mLastAnchorOffset = 0;
    311  }
    312 
    313  mAnchorNodeIsDirty = false;
    314 }
    315 
    316 void ScrollAnchorContainer::UserScrolled() {
    317  if (mApplyingAnchorAdjustment) {
    318    return;
    319  }
    320  InvalidateAnchor();
    321 
    322  if (!StaticPrefs::
    323          layout_css_scroll_anchoring_reset_heuristic_during_animation() &&
    324      Frame()->ScrollAnimationState().contains(
    325          ScrollContainerFrame::AnimationState::APZInProgress)) {
    326    // We'd want to skip resetting our heuristic while APZ is running an async
    327    // scroll because this UserScrolled function gets called on every refresh
    328    // driver's tick during running the async scroll, thus it will clobber the
    329    // heuristic.
    330    return;
    331  }
    332 
    333  mHeuristic.Reset();
    334 }
    335 
    336 void ScrollAnchorContainer::DisablingHeuristic::Reset() {
    337  mConsecutiveScrollAnchoringAdjustments = SaturateUint32(0);
    338  mConsecutiveScrollAnchoringAdjustmentLength = 0;
    339  mTimeStamp = {};
    340 }
    341 
    342 void ScrollAnchorContainer::AdjustmentMade(nscoord aAdjustment) {
    343  MOZ_ASSERT(!mDisabled, "How?");
    344  mDisabled = mHeuristic.AdjustmentMade(*this, aAdjustment);
    345 }
    346 
    347 bool ScrollAnchorContainer::DisablingHeuristic::AdjustmentMade(
    348    const ScrollAnchorContainer& aAnchor, nscoord aAdjustment) {
    349  // A reasonably large number of times that we want to check for this. If we
    350  // haven't hit this limit after these many attempts we assume we'll never hit
    351  // it.
    352  //
    353  // This is to prevent the number getting too large and making the limit round
    354  // to zero by mere precision error.
    355  //
    356  // 100k should be enough for anyone :)
    357  static const uint32_t kAnchorCheckCountLimit = 100000;
    358 
    359  // Zero-length adjustments are common & don't have side effects, so we don't
    360  // want them to consider them here; they'd bias our average towards 0.
    361  MOZ_ASSERT(aAdjustment, "Don't call this API for zero-length adjustments");
    362 
    363  const uint32_t maxConsecutiveAdjustments =
    364      StaticPrefs::layout_css_scroll_anchoring_max_consecutive_adjustments();
    365 
    366  if (!maxConsecutiveAdjustments) {
    367    return false;
    368  }
    369 
    370  // We don't high resolution for this timestamp.
    371  const auto now = TimeStamp::NowLoRes();
    372  if (mConsecutiveScrollAnchoringAdjustments++ == 0) {
    373    MOZ_ASSERT(mTimeStamp.IsNull());
    374    mTimeStamp = now;
    375  } else if (
    376      const auto timeoutMs = StaticPrefs::
    377          layout_css_scroll_anchoring_max_consecutive_adjustments_timeout_ms();
    378      timeoutMs && (now - mTimeStamp).ToMilliseconds() > timeoutMs) {
    379    Reset();
    380    return false;
    381  }
    382 
    383  mConsecutiveScrollAnchoringAdjustmentLength = NSCoordSaturatingAdd(
    384      mConsecutiveScrollAnchoringAdjustmentLength, aAdjustment);
    385 
    386  uint32_t consecutiveAdjustments =
    387      mConsecutiveScrollAnchoringAdjustments.value();
    388  if (consecutiveAdjustments < maxConsecutiveAdjustments ||
    389      consecutiveAdjustments > kAnchorCheckCountLimit) {
    390    return false;
    391  }
    392 
    393  auto cssPixels =
    394      CSSPixel::FromAppUnits(mConsecutiveScrollAnchoringAdjustmentLength);
    395  double average = double(cssPixels) / consecutiveAdjustments;
    396  uint32_t minAverage = StaticPrefs::
    397      layout_css_scroll_anchoring_min_average_adjustment_threshold();
    398  if (MOZ_LIKELY(std::abs(average) >= double(minAverage))) {
    399    return false;
    400  }
    401 
    402  ANCHOR_LOG_WITH(&aAnchor,
    403                  "Disabled scroll anchoring for container: "
    404                  "%f average, %f total out of %u consecutive adjustments\n",
    405                  average, float(cssPixels), consecutiveAdjustments);
    406 
    407  AutoTArray<nsString, 3> arguments;
    408  arguments.AppendElement()->AppendInt(consecutiveAdjustments);
    409  arguments.AppendElement()->AppendFloat(average);
    410  arguments.AppendElement()->AppendFloat(cssPixels);
    411 
    412  nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "Layout"_ns,
    413                                  aAnchor.Frame()->PresContext()->Document(),
    414                                  nsContentUtils::eLAYOUT_PROPERTIES,
    415                                  "ScrollAnchoringDisabledInContainer",
    416                                  arguments);
    417  return true;
    418 }
    419 
    420 void ScrollAnchorContainer::SuppressAdjustments() {
    421  ANCHOR_LOG("Received a scroll anchor suppression for %p.\n", this);
    422  mSuppressAnchorAdjustment = true;
    423 
    424  // Forward to our parent if appropriate, that is, if we don't maintain an
    425  // anchor, and we can't maintain one.
    426  //
    427  // Note that we need to check !CanMaintainAnchor(), instead of just whether
    428  // our frame is in the anchor chain of our ancestor as InvalidateAnchor()
    429  // does, given some suppression triggers apply even for nodes that are not in
    430  // the anchor chain.
    431  if (!mAnchorNode && !CanMaintainAnchor()) {
    432    if (ScrollAnchorContainer* container = FindFor(Frame())) {
    433      ANCHOR_LOG(" > Forwarding to parent anchor\n");
    434      container->SuppressAdjustments();
    435    }
    436  }
    437 }
    438 
    439 void ScrollAnchorContainer::InvalidateAnchor(ScheduleSelection aSchedule) {
    440  ANCHOR_LOG("Invalidating scroll anchor %p for %p.\n", mAnchorNode, this);
    441 
    442  if (mAnchorNode) {
    443    SetAnchorFlags(Frame()->mScrolledFrame, mAnchorNode, false);
    444  } else if (Frame()->mScrolledFrame->IsInScrollAnchorChain()) {
    445    ANCHOR_LOG(" > Forwarding to parent anchor\n");
    446    // We don't maintain an anchor, and our scrolled frame is in the anchor
    447    // chain of an ancestor. Invalidate that anchor.
    448    //
    449    // NOTE: Intentionally not forwarding aSchedule: Scheduling is always safe
    450    // and not doing so is just an optimization.
    451    FindFor(Frame())->InvalidateAnchor();
    452  }
    453  mAnchorNode = nullptr;
    454  mAnchorMightBeSubOptimal = false;
    455  mAnchorNodeIsDirty = true;
    456  mLastAnchorOffset = 0;
    457 
    458  if (!CanMaintainAnchor() || aSchedule == ScheduleSelection::No) {
    459    return;
    460  }
    461 
    462  Frame()->PresShell()->PostPendingScrollAnchorSelection(this);
    463 }
    464 
    465 void ScrollAnchorContainer::Destroy() {
    466  InvalidateAnchor(ScheduleSelection::No);
    467 }
    468 
    469 void ScrollAnchorContainer::ApplyAdjustments() {
    470  if (!mAnchorNode || mAnchorNodeIsDirty || mDisabled ||
    471      Frame()->HasPendingScrollRestoration() ||
    472      (StaticPrefs::
    473           layout_css_scroll_anchoring_reset_heuristic_during_animation() &&
    474       Frame()->IsProcessingScrollEvent()) ||
    475      Frame()->ScrollAnimationState().contains(
    476          ScrollContainerFrame::AnimationState::TriggeredByScript) ||
    477      Frame()->GetScrollPosition() == nsPoint()) {
    478    ANCHOR_LOG(
    479        "Ignoring post-reflow (anchor=%p, dirty=%d, disabled=%d, "
    480        "pendingRestoration=%d, scrollevent=%d, scriptAnimating=%d, "
    481        "zeroScrollPos=%d pendingSuppression=%d, "
    482        "container=%p).\n",
    483        mAnchorNode, mAnchorNodeIsDirty, mDisabled,
    484        Frame()->HasPendingScrollRestoration(),
    485        Frame()->IsProcessingScrollEvent(),
    486        Frame()->ScrollAnimationState().contains(
    487            ScrollContainerFrame::AnimationState::TriggeredByScript),
    488        Frame()->GetScrollPosition() == nsPoint(), mSuppressAnchorAdjustment,
    489        this);
    490    if (mSuppressAnchorAdjustment) {
    491      mSuppressAnchorAdjustment = false;
    492      InvalidateAnchor();
    493    }
    494    return;
    495  }
    496 
    497  nscoord current = FindScrollAnchoringBoundingOffset(Frame(), mAnchorNode);
    498  nscoord logicalAdjustment = current - mLastAnchorOffset;
    499  WritingMode writingMode = Frame()->GetWritingMode();
    500 
    501  ANCHOR_LOG("Anchor has moved from %s to %s.\n",
    502             ToString(CSSPixel::FromAppUnits(mLastAnchorOffset)).c_str(),
    503             ToString(CSSPixel::FromAppUnits(current)).c_str());
    504 
    505  auto maybeInvalidate = MakeScopeExit([&] {
    506    if (mAnchorMightBeSubOptimal &&
    507        StaticPrefs::layout_css_scroll_anchoring_reselect_if_suboptimal()) {
    508      ANCHOR_LOG(
    509          "Anchor might be suboptimal, invalidating to try finding a better "
    510          "one\n");
    511      InvalidateAnchor();
    512    }
    513  });
    514 
    515  if (logicalAdjustment == 0) {
    516    ANCHOR_LOG("Ignoring zero delta anchor adjustment for %p.\n", this);
    517    mSuppressAnchorAdjustment = false;
    518    return;
    519  }
    520 
    521  if (mSuppressAnchorAdjustment) {
    522    ANCHOR_LOG("Applying anchor adjustment suppression for %p.\n", this);
    523    mSuppressAnchorAdjustment = false;
    524    InvalidateAnchor();
    525    return;
    526  }
    527 
    528  ANCHOR_LOG("Applying anchor adjustment of %s in %s with anchor %p.\n",
    529             ToString(CSSPixel::FromAppUnits(logicalAdjustment)).c_str(),
    530             ToString(writingMode).c_str(), mAnchorNode);
    531 
    532  AdjustmentMade(logicalAdjustment);
    533 
    534  nsPoint physicalAdjustment;
    535  switch (writingMode.GetBlockDir()) {
    536    case WritingMode::BlockDir::TB: {
    537      physicalAdjustment.y = logicalAdjustment;
    538      break;
    539    }
    540    case WritingMode::BlockDir::LR: {
    541      physicalAdjustment.x = logicalAdjustment;
    542      break;
    543    }
    544    case WritingMode::BlockDir::RL: {
    545      physicalAdjustment.x = -logicalAdjustment;
    546      break;
    547    }
    548  }
    549 
    550  MOZ_RELEASE_ASSERT(!mApplyingAnchorAdjustment);
    551  // We should use AutoRestore here, but that doesn't work with bitfields
    552  mApplyingAnchorAdjustment = true;
    553  Frame()->ScrollToInternal(Frame()->GetScrollPosition() + physicalAdjustment,
    554                            ScrollMode::Instant, ScrollOrigin::Relative);
    555  mApplyingAnchorAdjustment = false;
    556 
    557  if (Frame()->mIsRoot) {
    558    Frame()->PresShell()->RootScrollFrameAdjusted(physicalAdjustment.y);
    559  }
    560 
    561  // The anchor position may not be in the same relative position after
    562  // adjustment. Update ourselves so we have consistent state.
    563  mLastAnchorOffset = FindScrollAnchoringBoundingOffset(Frame(), mAnchorNode);
    564 }
    565 
    566 ScrollAnchorContainer::ExamineResult
    567 ScrollAnchorContainer::ExamineAnchorCandidate(nsIFrame* aFrame) const {
    568 #ifdef DEBUG_FRAME_DUMP
    569  nsCString tag = aFrame->ListTag();
    570  ANCHOR_LOG("\tVisiting frame=%s (%p).\n", tag.get(), aFrame);
    571 #else
    572  ANCHOR_LOG("\t\tVisiting frame=%p.\n", aFrame);
    573 #endif
    574  bool isText = !!Text::FromNodeOrNull(aFrame->GetContent());
    575  bool isContinuation = !!aFrame->GetPrevContinuation();
    576 
    577  if (isText && isContinuation) {
    578    ANCHOR_LOG("\t\tExcluding continuation text node.\n");
    579    return ExamineResult::Exclude;
    580  }
    581 
    582  // Check if the author has opted out of scroll anchoring for this frame
    583  // and its descendants.
    584  const nsStyleDisplay* disp = aFrame->StyleDisplay();
    585  if (disp->mOverflowAnchor == mozilla::StyleOverflowAnchor::None) {
    586    ANCHOR_LOG("\t\tExcluding `overflow-anchor: none`.\n");
    587    return ExamineResult::Exclude;
    588  }
    589 
    590  // Sticky positioned elements can move with the scroll frame, making them
    591  // unsuitable scroll anchors. This isn't in the specification yet [1], but
    592  // matches Blink's implementation.
    593  //
    594  // [1] https://github.com/w3c/csswg-drafts/issues/3319
    595  if (aFrame->IsStickyPositioned()) {
    596    ANCHOR_LOG("\t\tExcluding `position: sticky`.\n");
    597    return ExamineResult::Exclude;
    598  }
    599 
    600  // The frame for a <br> element has a non-zero area, but Blink treats them
    601  // as if they have no area, so exclude them specially.
    602  if (aFrame->IsBrFrame()) {
    603    ANCHOR_LOG("\t\tExcluding <br>.\n");
    604    return ExamineResult::Exclude;
    605  }
    606 
    607  // Exclude frames that aren't accessible to content.
    608  bool isChrome =
    609      aFrame->GetContent() && aFrame->GetContent()->ChromeOnlyAccess();
    610  bool isPseudo = aFrame->Style()->IsPseudoElement();
    611  if (isChrome && !isPseudo) {
    612    ANCHOR_LOG("\t\tExcluding chrome only content.\n");
    613    return ExamineResult::Exclude;
    614  }
    615 
    616  const bool isReplaced = aFrame->IsReplaced();
    617  const bool isNonReplacedInline =
    618      aFrame->StyleDisplay()->IsInlineInsideStyle() && !isReplaced;
    619 
    620  const bool isAnonBox = aFrame->Style()->IsAnonBox();
    621 
    622  // See if this frame has or could maintain its own anchor node.
    623  const bool isScrollableWithAnchor = [&] {
    624    ScrollContainerFrame* scrollContainer = do_QueryFrame(aFrame);
    625    if (!scrollContainer) {
    626      return false;
    627    }
    628    auto* anchor = scrollContainer->Anchor();
    629    return anchor->AnchorNode() || anchor->CanMaintainAnchor();
    630  }();
    631 
    632  // We don't allow scroll anchors to be selected inside of nested scrollable
    633  // frames which maintain an anchor node as it's not clear how an anchor
    634  // adjustment should apply to multiple scrollable frames.
    635  //
    636  // It is important to descend into _some_ scrollable frames, specially
    637  // overflow: hidden, as those don't generally maintain their own anchors, and
    638  // it is a common case in the wild where scroll anchoring ought to work.
    639  //
    640  // We also don't allow scroll anchors to be selected inside of replaced
    641  // elements (like <img>, <video>, <svg>...) as they behave atomically. SVG
    642  // uses a different layout model than CSS, and the specification doesn't say
    643  // it should apply anyway.
    644  //
    645  // [1] https://github.com/w3c/csswg-drafts/issues/3477
    646  const bool canDescend = !isScrollableWithAnchor && !isReplaced;
    647 
    648  // Non-replaced inline boxes (including ruby frames) and anon boxes are not
    649  // acceptable anchors, so we descend if possible, or otherwise exclude them
    650  // altogether.
    651  if (!isText && (isNonReplacedInline || isAnonBox)) {
    652    ANCHOR_LOG(
    653        "\t\tSearching descendants of anon or non-replaced inline box (a=%d, "
    654        "i=%d).\n",
    655        isAnonBox, isNonReplacedInline);
    656    if (canDescend) {
    657      return ExamineResult::PassThrough;
    658    }
    659    return ExamineResult::Exclude;
    660  }
    661 
    662  // Find the scroll anchoring bounding rect.
    663  nsRect rect = FindScrollAnchoringBoundingRect(Frame(), aFrame);
    664  ANCHOR_LOG("\t\trect = %s.\n", ToString(CSSRect::FromAppUnits(rect)).c_str());
    665 
    666  // Check if this frame is visible in the scroll port. This will exclude rects
    667  // with zero sized area. The specification is ambiguous about this [1], but
    668  // this matches Blink's implementation.
    669  //
    670  // [1] https://github.com/w3c/csswg-drafts/issues/3483
    671  nsRect visibleRect;
    672  if (!visibleRect.IntersectRect(rect,
    673                                 Frame()->GetVisualOptimalViewingRect())) {
    674    return ExamineResult::Exclude;
    675  }
    676 
    677  // It's not clear what the scroll anchoring bounding rect is, for elements
    678  // fragmented in the block direction (e.g. across column or page breaks).
    679  //
    680  // Inline-fragmented elements other than text shouldn't get here because of
    681  // the isNonReplacedInline check.
    682  //
    683  // For text nodes that are fragmented, it's specified that we need to consider
    684  // the union of its line boxes.
    685  //
    686  // So for text nodes we handle them by including the union of line boxes in
    687  // the bounding rect of the primary frame, and not selecting any
    688  // continuations.
    689  //
    690  // For block-outside elements we choose to consider the bounding rect of each
    691  // frame individually, allowing ourselves to descend into any frame, but only
    692  // selecting a frame if it's not a continuation.
    693  if (canDescend && isContinuation) {
    694    ANCHOR_LOG("\t\tSearching descendants of a continuation.\n");
    695    return ExamineResult::PassThrough;
    696  }
    697 
    698  // If this frame is fully visible, then select it as the scroll anchor.
    699  if (visibleRect.IsEqualEdges(rect)) {
    700    ANCHOR_LOG("\t\tFully visible, taking.\n");
    701    return ExamineResult::Accept;
    702  }
    703 
    704  // If we can't descend into this frame, then select it as the scroll anchor.
    705  if (!canDescend) {
    706    ANCHOR_LOG("\t\tIntersects a frame that we can't descend into, taking.\n");
    707    return ExamineResult::Accept;
    708  }
    709 
    710  // It must be partially visible and we can descend into this frame. Examine
    711  // its children for a better scroll anchor or fall back to this one.
    712  ANCHOR_LOG("\t\tIntersects valid candidate, checking descendants.\n");
    713  return ExamineResult::Traverse;
    714 }
    715 
    716 nsIFrame* ScrollAnchorContainer::FindAnchorIn(nsIFrame* aFrame) const {
    717  // Visit the child lists of this frame
    718  for (const auto& [list, listID] : aFrame->ChildLists()) {
    719    // Skip child lists that contain out-of-flow frames, we'll visit them by
    720    // following placeholders in the in-flow lists so that we visit these
    721    // frames in DOM order.
    722    // XXX do we actually need to exclude FrameChildListID::OverflowOutOfFlow
    723    // too?
    724    if (listID == FrameChildListID::Absolute ||
    725        listID == FrameChildListID::Fixed ||
    726        listID == FrameChildListID::Float ||
    727        listID == FrameChildListID::OverflowOutOfFlow) {
    728      continue;
    729    }
    730 
    731    // Search the child list, and return if we selected an anchor
    732    if (nsIFrame* anchor = FindAnchorInList(list)) {
    733      return anchor;
    734    }
    735  }
    736 
    737  // The spec requires us to do an extra pass to visit absolutely positioned
    738  // frames a second time after all the children of their containing block have
    739  // been visited.
    740  //
    741  // It's not clear why this is needed [1], but it matches Blink's
    742  // implementation, and is needed for a WPT test.
    743  //
    744  // [1] https://github.com/w3c/csswg-drafts/issues/3465
    745  const nsFrameList& absPosList =
    746      aFrame->GetChildList(FrameChildListID::Absolute);
    747  if (nsIFrame* anchor = FindAnchorInList(absPosList)) {
    748    return anchor;
    749  }
    750 
    751  return nullptr;
    752 }
    753 
    754 nsIFrame* ScrollAnchorContainer::FindAnchorInList(
    755    const nsFrameList& aFrameList) const {
    756  for (nsIFrame* child : aFrameList) {
    757    // If this is a placeholder, try to follow it to the out of flow frame.
    758    nsIFrame* realFrame = nsPlaceholderFrame::GetRealFrameFor(child);
    759    if (child != realFrame) {
    760      // If the out of flow frame is not a descendant of our scroll frame,
    761      // then it must have a different containing block and cannot be an
    762      // anchor node.
    763      if (!nsLayoutUtils::IsProperAncestorFrame(Frame(), realFrame)) {
    764        ANCHOR_LOG(
    765            "\t\tSkipping out of flow frame that is not a descendant of the "
    766            "scroll frame.\n");
    767        continue;
    768      }
    769      ANCHOR_LOG("\t\tFollowing placeholder to out of flow frame.\n");
    770      child = realFrame;
    771    }
    772 
    773    // Perform the candidate examination algorithm
    774    ExamineResult examine = ExamineAnchorCandidate(child);
    775 
    776    // See the comment before the definition of `ExamineResult` in
    777    // `ScrollAnchorContainer.h` for an explanation of this behavior.
    778    switch (examine) {
    779      case ExamineResult::Exclude: {
    780        continue;
    781      }
    782      case ExamineResult::PassThrough: {
    783        nsIFrame* candidate = FindAnchorIn(child);
    784        if (!candidate) {
    785          continue;
    786        }
    787        return candidate;
    788      }
    789      case ExamineResult::Traverse: {
    790        nsIFrame* candidate = FindAnchorIn(child);
    791        if (!candidate) {
    792          return child;
    793        }
    794        return candidate;
    795      }
    796      case ExamineResult::Accept: {
    797        return child;
    798      }
    799    }
    800  }
    801  return nullptr;
    802 }
    803 
    804 }  // namespace mozilla::layout