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 "ScrollAnchorContainer.h"
10 #include "mozilla/dom/Text.h"
11 #include "mozilla/ScopeExit.h"
12 #include "mozilla/PresShell.h"
13 #include "mozilla/ProfilerLabels.h"
14 #include "mozilla/StaticPrefs_layout.h"
15 #include "mozilla/ToString.h"
16 #include "nsBlockFrame.h"
17 #include "nsGfxScrollFrame.h"
19 #include "nsIFrameInlines.h"
20 #include "nsLayoutUtils.h"
21 #include "nsPlaceholderFrame.h"
23 using namespace mozilla::dom
;
26 static mozilla::LazyLogModule
sAnchorLog("scrollanchor");
28 # define ANCHOR_LOG_WITH(anchor_, fmt, ...) \
29 MOZ_LOG(sAnchorLog, LogLevel::Debug, \
30 ("ANCHOR(%p, %s, root: %d): " fmt, (anchor_), \
36 ->GetSpecOrDefault() \
38 (anchor_)->Frame()->mIsRoot, ##__VA_ARGS__));
40 # define ANCHOR_LOG(fmt, ...) ANCHOR_LOG_WITH(this, fmt, ##__VA_ARGS__)
42 # define ANCHOR_LOG(...)
43 # define ANCHOR_LOG_WITH(...)
46 namespace mozilla::layout
{
48 nsHTMLScrollFrame
* ScrollAnchorContainer::Frame() const {
49 return reinterpret_cast<nsHTMLScrollFrame
*>(
50 ((char*)this) - offsetof(nsHTMLScrollFrame
, mAnchor
));
53 ScrollAnchorContainer::ScrollAnchorContainer(nsHTMLScrollFrame
* aScrollFrame
)
55 mAnchorMightBeSubOptimal(false),
56 mAnchorNodeIsDirty(true),
57 mApplyingAnchorAdjustment(false),
58 mSuppressAnchorAdjustment(false) {
59 MOZ_ASSERT(aScrollFrame
== Frame());
62 ScrollAnchorContainer::~ScrollAnchorContainer() = default;
64 ScrollAnchorContainer
* ScrollAnchorContainer::FindFor(nsIFrame
* aFrame
) {
65 aFrame
= aFrame
->GetParent();
69 nsIScrollableFrame
* nearest
= nsLayoutUtils::GetNearestScrollableFrame(
70 aFrame
, nsLayoutUtils::SCROLLABLE_SAME_DOC
|
71 nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN
);
73 return nearest
->Anchor();
78 nsIScrollableFrame
* ScrollAnchorContainer::ScrollableFrame() const {
79 return Frame()->GetScrollTargetFrame();
83 * Set the appropriate frame flags for a frame that has become or is no longer
86 static void SetAnchorFlags(const nsIFrame
* aScrolledFrame
,
87 nsIFrame
* aAnchorNode
, bool aInScrollAnchorChain
) {
88 nsIFrame
* frame
= aAnchorNode
;
89 while (frame
&& frame
!= aScrolledFrame
) {
90 // TODO(emilio, bug 1629280): This commented out assertion below should
91 // hold, but it may not in the case of reparenting-during-reflow (due to
92 // inline fragmentation or such). That looks fishy!
94 // We should either invalidate the anchor when reparenting any frame on the
95 // chain, or fix up the chain flags.
97 // MOZ_DIAGNOSTIC_ASSERT(frame->IsInScrollAnchorChain() !=
98 // aInScrollAnchorChain);
99 frame
->SetInScrollAnchorChain(aInScrollAnchorChain
);
100 frame
= frame
->GetParent();
103 "The anchor node should be a descendant of the scrolled frame");
104 // If needed, invalidate the frame so that we start/stop highlighting the
106 if (StaticPrefs::layout_css_scroll_anchoring_highlight()) {
107 for (nsIFrame
* frame
= aAnchorNode
->FirstContinuation(); !!frame
;
108 frame
= frame
->GetNextContinuation()) {
109 frame
->InvalidateFrame();
115 * Compute the scrollable overflow rect [1] of aCandidate relative to
116 * aScrollFrame with all transforms applied.
118 * The specification is ambiguous about what can be selected as a scroll anchor,
119 * which makes the scroll anchoring bounding rect partially undefined [2]. This
120 * code attempts to match the implementation in Blink.
122 * An additional unspecified behavior is that any scrollable overflow before the
123 * border start edge in the block axis of aScrollFrame should be clamped. This
124 * is to prevent absolutely positioned descendant elements from being able to
125 * trigger scroll adjustments [3].
128 * https://drafts.csswg.org/css-scroll-anchoring-1/#scroll-anchoring-bounding-rect
129 * [2] https://github.com/w3c/csswg-drafts/issues/3478
130 * [3] https://bugzilla.mozilla.org/show_bug.cgi?id=1519541
132 static nsRect
FindScrollAnchoringBoundingRect(const nsIFrame
* aScrollFrame
,
133 nsIFrame
* aCandidate
) {
134 MOZ_ASSERT(nsLayoutUtils::IsProperAncestorFrame(aScrollFrame
, aCandidate
));
135 if (!!Text::FromNodeOrNull(aCandidate
->GetContent())) {
136 // This is a frame for a text node. The spec says we need to accumulate the
137 // union of all line boxes in the coordinate space of the scroll frame
138 // accounting for transforms.
140 // To do this, we translate and accumulate the overflow rect for each text
141 // continuation to the coordinate space of the nearest ancestor block
142 // frame. Then we transform the resulting rect into the coordinate space of
145 // Transforms aren't allowed on non-replaced inline boxes, so we can assume
146 // that these text node continuations will have the same transform as their
147 // nearest block ancestor. And it should be faster to transform their union
148 // rather than individually transforming each overflow rect
150 // XXX for fragmented blocks, blockAncestor will be an ancestor only to the
151 // text continuations in the first block continuation. GetOffsetTo
152 // should continue to work, but is it correct with transforms or a
153 // performance hazard?
154 nsIFrame
* blockAncestor
=
155 nsLayoutUtils::FindNearestBlockAncestor(aCandidate
);
157 nsLayoutUtils::IsProperAncestorFrame(aScrollFrame
, blockAncestor
));
159 for (nsIFrame
* continuation
= aCandidate
->FirstContinuation(); continuation
;
160 continuation
= continuation
->GetNextContinuation()) {
161 nsRect overflowRect
=
162 continuation
->ScrollableOverflowRectRelativeToSelf();
163 overflowRect
+= continuation
->GetOffsetTo(blockAncestor
);
164 bounding
= bounding
.Union(overflowRect
);
166 return nsLayoutUtils::TransformFrameRectToAncestor(blockAncestor
, bounding
,
170 nsRect borderRect
= aCandidate
->GetRectRelativeToSelf();
171 nsRect overflowRect
= aCandidate
->ScrollableOverflowRectRelativeToSelf();
173 NS_ASSERTION(overflowRect
.Contains(borderRect
),
174 "overflow rect must include border rect, and the clamping logic "
175 "here depends on that");
177 // Clamp the scrollable overflow rect to the border start edge on the block
178 // axis of the scroll frame
179 WritingMode writingMode
= aScrollFrame
->GetWritingMode();
180 switch (writingMode
.GetBlockDir()) {
181 case WritingMode::eBlockTB
: {
182 overflowRect
.SetBoxY(borderRect
.Y(), overflowRect
.YMost());
185 case WritingMode::eBlockLR
: {
186 overflowRect
.SetBoxX(borderRect
.X(), overflowRect
.XMost());
189 case WritingMode::eBlockRL
: {
190 overflowRect
.SetBoxX(overflowRect
.X(), borderRect
.XMost());
195 nsRect transformed
= nsLayoutUtils::TransformFrameRectToAncestor(
196 aCandidate
, overflowRect
, aScrollFrame
);
201 * Compute the offset between the scrollable overflow rect start edge of
202 * aCandidate and the scroll-port start edge of aScrollFrame, in the block axis
205 static nscoord
FindScrollAnchoringBoundingOffset(
206 const nsHTMLScrollFrame
* aScrollFrame
, nsIFrame
* aCandidate
) {
207 WritingMode writingMode
= aScrollFrame
->GetWritingMode();
208 nsRect physicalBounding
=
209 FindScrollAnchoringBoundingRect(aScrollFrame
, aCandidate
);
210 LogicalRect
logicalBounding(writingMode
, physicalBounding
,
211 aScrollFrame
->mScrolledFrame
->GetSize());
212 return logicalBounding
.BStart(writingMode
);
215 bool ScrollAnchorContainer::CanMaintainAnchor() const {
216 if (!StaticPrefs::layout_css_scroll_anchoring_enabled()) {
220 // If we've been disabled due to heuristics, we don't anchor anymore.
225 const nsStyleDisplay
& disp
= *Frame()->StyleDisplay();
226 // Don't select a scroll anchor if the scroll frame has `overflow-anchor:
228 if (disp
.mOverflowAnchor
!= mozilla::StyleOverflowAnchor::Auto
) {
232 // Or if the scroll frame has not been scrolled from the logical origin. This
233 // is not in the specification [1], but Blink does this.
235 // [1] https://github.com/w3c/csswg-drafts/issues/3319
236 if (Frame()->GetLogicalScrollPosition() == nsPoint()) {
240 // Or if there is perspective that could affect the scrollable overflow rect
241 // for descendant frames. This is not in the specification as Blink doesn't
242 // share this behavior with perspective [1].
244 // [1] https://github.com/w3c/csswg-drafts/issues/3322
245 if (Frame()->ChildrenHavePerspective()) {
252 void ScrollAnchorContainer::SelectAnchor() {
253 MOZ_ASSERT(Frame()->mScrolledFrame
);
254 MOZ_ASSERT(mAnchorNodeIsDirty
);
256 AUTO_PROFILER_LABEL("ScrollAnchorContainer::SelectAnchor", LAYOUT
);
257 ANCHOR_LOG("Selecting anchor with scroll-port=%s.\n",
258 mozilla::ToString(Frame()->GetVisualOptimalViewingRect()).c_str());
260 // Select a new scroll anchor
261 nsIFrame
* oldAnchor
= mAnchorNode
;
262 if (CanMaintainAnchor()) {
263 MOZ_DIAGNOSTIC_ASSERT(
264 !Frame()->mScrolledFrame
->IsInScrollAnchorChain(),
265 "Our scrolled frame can't serve as or contain an anchor for an "
266 "ancestor if it can maintain its own anchor");
267 ANCHOR_LOG("Beginning selection.\n");
268 mAnchorNode
= FindAnchorIn(Frame()->mScrolledFrame
);
270 ANCHOR_LOG("Skipping selection, doesn't maintain a scroll anchor.\n");
271 mAnchorNode
= nullptr;
273 mAnchorMightBeSubOptimal
=
274 mAnchorNode
&& mAnchorNode
->HasAnyStateBits(NS_FRAME_HAS_DIRTY_CHILDREN
);
276 // Update the anchor flags if needed
277 if (oldAnchor
!= mAnchorNode
) {
278 ANCHOR_LOG("Anchor node has changed from (%p) to (%p).\n", oldAnchor
,
281 // Unset all flags for the old scroll anchor
283 SetAnchorFlags(Frame()->mScrolledFrame
, oldAnchor
, false);
286 // Set all flags for the new scroll anchor
288 // Anchor selection will never select a descendant of a nested scroll
289 // frame which maintains an anchor, so we can set flags without
290 // conflicting with other scroll anchor containers.
291 SetAnchorFlags(Frame()->mScrolledFrame
, mAnchorNode
, true);
294 ANCHOR_LOG("Anchor node has remained (%p).\n", mAnchorNode
);
297 // Calculate the position to use for scroll adjustments
299 mLastAnchorOffset
= FindScrollAnchoringBoundingOffset(Frame(), mAnchorNode
);
300 ANCHOR_LOG("Using last anchor offset = %d.\n", mLastAnchorOffset
);
302 mLastAnchorOffset
= 0;
305 mAnchorNodeIsDirty
= false;
308 void ScrollAnchorContainer::UserScrolled() {
309 if (mApplyingAnchorAdjustment
) {
315 layout_css_scroll_anchoring_reset_heuristic_during_animation() &&
316 Frame()->ScrollAnimationState().contains(
317 nsIScrollableFrame::AnimationState::APZInProgress
)) {
318 // We'd want to skip resetting our heuristic while APZ is running an async
319 // scroll because this UserScrolled function gets called on every refresh
320 // driver's tick during running the async scroll, thus it will clobber the
328 void ScrollAnchorContainer::DisablingHeuristic::Reset() {
329 mConsecutiveScrollAnchoringAdjustments
= SaturateUint32(0);
330 mConsecutiveScrollAnchoringAdjustmentLength
= 0;
334 void ScrollAnchorContainer::AdjustmentMade(nscoord aAdjustment
) {
335 MOZ_ASSERT(!mDisabled
, "How?");
336 mDisabled
= mHeuristic
.AdjustmentMade(*this, aAdjustment
);
339 bool ScrollAnchorContainer::DisablingHeuristic::AdjustmentMade(
340 const ScrollAnchorContainer
& aAnchor
, nscoord aAdjustment
) {
341 // A reasonably large number of times that we want to check for this. If we
342 // haven't hit this limit after these many attempts we assume we'll never hit
345 // This is to prevent the number getting too large and making the limit round
346 // to zero by mere precision error.
348 // 100k should be enough for anyone :)
349 static const uint32_t kAnchorCheckCountLimit
= 100000;
351 // Zero-length adjustments are common & don't have side effects, so we don't
352 // want them to consider them here; they'd bias our average towards 0.
353 MOZ_ASSERT(aAdjustment
, "Don't call this API for zero-length adjustments");
355 const uint32_t maxConsecutiveAdjustments
=
356 StaticPrefs::layout_css_scroll_anchoring_max_consecutive_adjustments();
358 if (!maxConsecutiveAdjustments
) {
362 // We don't high resolution for this timestamp.
363 const auto now
= TimeStamp::NowLoRes();
364 if (mConsecutiveScrollAnchoringAdjustments
++ == 0) {
365 MOZ_ASSERT(mTimeStamp
.IsNull());
368 const auto timeoutMs
= StaticPrefs::
369 layout_css_scroll_anchoring_max_consecutive_adjustments_timeout_ms();
370 timeoutMs
&& (now
- mTimeStamp
).ToMilliseconds() > timeoutMs
) {
375 mConsecutiveScrollAnchoringAdjustmentLength
= NSCoordSaturatingAdd(
376 mConsecutiveScrollAnchoringAdjustmentLength
, aAdjustment
);
378 uint32_t consecutiveAdjustments
=
379 mConsecutiveScrollAnchoringAdjustments
.value();
380 if (consecutiveAdjustments
< maxConsecutiveAdjustments
||
381 consecutiveAdjustments
> kAnchorCheckCountLimit
) {
386 CSSPixel::FromAppUnits(mConsecutiveScrollAnchoringAdjustmentLength
);
387 double average
= double(cssPixels
) / consecutiveAdjustments
;
388 uint32_t minAverage
= StaticPrefs::
389 layout_css_scroll_anchoring_min_average_adjustment_threshold();
390 if (MOZ_LIKELY(std::abs(average
) >= double(minAverage
))) {
394 ANCHOR_LOG_WITH(&aAnchor
,
395 "Disabled scroll anchoring for container: "
396 "%f average, %f total out of %u consecutive adjustments\n",
397 average
, float(cssPixels
), consecutiveAdjustments
);
399 AutoTArray
<nsString
, 3> arguments
;
400 arguments
.AppendElement()->AppendInt(consecutiveAdjustments
);
401 arguments
.AppendElement()->AppendFloat(average
);
402 arguments
.AppendElement()->AppendFloat(cssPixels
);
404 nsContentUtils::ReportToConsole(nsIScriptError::warningFlag
, "Layout"_ns
,
405 aAnchor
.Frame()->PresContext()->Document(),
406 nsContentUtils::eLAYOUT_PROPERTIES
,
407 "ScrollAnchoringDisabledInContainer",
412 void ScrollAnchorContainer::SuppressAdjustments() {
413 ANCHOR_LOG("Received a scroll anchor suppression for %p.\n", this);
414 mSuppressAnchorAdjustment
= true;
416 // Forward to our parent if appropriate, that is, if we don't maintain an
417 // anchor, and we can't maintain one.
419 // Note that we need to check !CanMaintainAnchor(), instead of just whether
420 // our frame is in the anchor chain of our ancestor as InvalidateAnchor()
421 // does, given some suppression triggers apply even for nodes that are not in
423 if (!mAnchorNode
&& !CanMaintainAnchor()) {
424 if (ScrollAnchorContainer
* container
= FindFor(Frame())) {
425 ANCHOR_LOG(" > Forwarding to parent anchor\n");
426 container
->SuppressAdjustments();
431 void ScrollAnchorContainer::InvalidateAnchor(ScheduleSelection aSchedule
) {
432 ANCHOR_LOG("Invalidating scroll anchor %p for %p.\n", mAnchorNode
, this);
435 SetAnchorFlags(Frame()->mScrolledFrame
, mAnchorNode
, false);
436 } else if (Frame()->mScrolledFrame
->IsInScrollAnchorChain()) {
437 ANCHOR_LOG(" > Forwarding to parent anchor\n");
438 // We don't maintain an anchor, and our scrolled frame is in the anchor
439 // chain of an ancestor. Invalidate that anchor.
441 // NOTE: Intentionally not forwarding aSchedule: Scheduling is always safe
442 // and not doing so is just an optimization.
443 FindFor(Frame())->InvalidateAnchor();
445 mAnchorNode
= nullptr;
446 mAnchorMightBeSubOptimal
= false;
447 mAnchorNodeIsDirty
= true;
448 mLastAnchorOffset
= 0;
450 if (!CanMaintainAnchor() || aSchedule
== ScheduleSelection::No
) {
454 Frame()->PresShell()->PostPendingScrollAnchorSelection(this);
457 void ScrollAnchorContainer::Destroy() {
458 InvalidateAnchor(ScheduleSelection::No
);
461 void ScrollAnchorContainer::ApplyAdjustments() {
462 if (!mAnchorNode
|| mAnchorNodeIsDirty
|| mDisabled
||
463 Frame()->HasPendingScrollRestoration() ||
465 layout_css_scroll_anchoring_reset_heuristic_during_animation() &&
466 Frame()->IsProcessingScrollEvent()) ||
467 Frame()->ScrollAnimationState().contains(
468 nsIScrollableFrame::AnimationState::TriggeredByScript
) ||
469 Frame()->GetScrollPosition() == nsPoint()) {
471 "Ignoring post-reflow (anchor=%p, dirty=%d, disabled=%d, "
472 "pendingRestoration=%d, scrollevent=%d, scriptAnimating=%d, "
473 "zeroScrollPos=%d pendingSuppression=%d, "
475 mAnchorNode
, mAnchorNodeIsDirty
, mDisabled
,
476 Frame()->HasPendingScrollRestoration(),
477 Frame()->IsProcessingScrollEvent(),
478 Frame()->ScrollAnimationState().contains(
479 nsIScrollableFrame::AnimationState::TriggeredByScript
),
480 Frame()->GetScrollPosition() == nsPoint(), mSuppressAnchorAdjustment
,
482 if (mSuppressAnchorAdjustment
) {
483 mSuppressAnchorAdjustment
= false;
489 nscoord current
= FindScrollAnchoringBoundingOffset(Frame(), mAnchorNode
);
490 nscoord logicalAdjustment
= current
- mLastAnchorOffset
;
491 WritingMode writingMode
= Frame()->GetWritingMode();
493 ANCHOR_LOG("Anchor has moved from %d to %d.\n", mLastAnchorOffset
, current
);
495 auto maybeInvalidate
= MakeScopeExit([&] {
496 if (mAnchorMightBeSubOptimal
&&
497 StaticPrefs::layout_css_scroll_anchoring_reselect_if_suboptimal()) {
499 "Anchor might be suboptimal, invalidating to try finding a better "
505 if (logicalAdjustment
== 0) {
506 ANCHOR_LOG("Ignoring zero delta anchor adjustment for %p.\n", this);
507 mSuppressAnchorAdjustment
= false;
511 if (mSuppressAnchorAdjustment
) {
512 ANCHOR_LOG("Applying anchor adjustment suppression for %p.\n", this);
513 mSuppressAnchorAdjustment
= false;
518 ANCHOR_LOG("Applying anchor adjustment of %d in %s with anchor %p.\n",
519 logicalAdjustment
, ToString(writingMode
).c_str(), mAnchorNode
);
521 AdjustmentMade(logicalAdjustment
);
523 nsPoint physicalAdjustment
;
524 switch (writingMode
.GetBlockDir()) {
525 case WritingMode::eBlockTB
: {
526 physicalAdjustment
.y
= logicalAdjustment
;
529 case WritingMode::eBlockLR
: {
530 physicalAdjustment
.x
= logicalAdjustment
;
533 case WritingMode::eBlockRL
: {
534 physicalAdjustment
.x
= -logicalAdjustment
;
539 MOZ_RELEASE_ASSERT(!mApplyingAnchorAdjustment
);
540 // We should use AutoRestore here, but that doesn't work with bitfields
541 mApplyingAnchorAdjustment
= true;
542 Frame()->ScrollToInternal(Frame()->GetScrollPosition() + physicalAdjustment
,
543 ScrollMode::Instant
, ScrollOrigin::Relative
);
544 mApplyingAnchorAdjustment
= false;
546 if (Frame()->mIsRoot
) {
547 Frame()->PresShell()->RootScrollFrameAdjusted(physicalAdjustment
.y
);
550 // The anchor position may not be in the same relative position after
551 // adjustment. Update ourselves so we have consistent state.
552 mLastAnchorOffset
= FindScrollAnchoringBoundingOffset(Frame(), mAnchorNode
);
555 ScrollAnchorContainer::ExamineResult
556 ScrollAnchorContainer::ExamineAnchorCandidate(nsIFrame
* aFrame
) const {
557 #ifdef DEBUG_FRAME_DUMP
558 nsCString tag
= aFrame
->ListTag();
559 ANCHOR_LOG("\tVisiting frame=%s (%p).\n", tag
.get(), aFrame
);
561 ANCHOR_LOG("\t\tVisiting frame=%p.\n", aFrame
);
563 bool isText
= !!Text::FromNodeOrNull(aFrame
->GetContent());
564 bool isContinuation
= !!aFrame
->GetPrevContinuation();
566 if (isText
&& isContinuation
) {
567 ANCHOR_LOG("\t\tExcluding continuation text node.\n");
568 return ExamineResult::Exclude
;
571 // Check if the author has opted out of scroll anchoring for this frame
572 // and its descendants.
573 const nsStyleDisplay
* disp
= aFrame
->StyleDisplay();
574 if (disp
->mOverflowAnchor
== mozilla::StyleOverflowAnchor::None
) {
575 ANCHOR_LOG("\t\tExcluding `overflow-anchor: none`.\n");
576 return ExamineResult::Exclude
;
579 // Sticky positioned elements can move with the scroll frame, making them
580 // unsuitable scroll anchors. This isn't in the specification yet [1], but
581 // matches Blink's implementation.
583 // [1] https://github.com/w3c/csswg-drafts/issues/3319
584 if (aFrame
->IsStickyPositioned()) {
585 ANCHOR_LOG("\t\tExcluding `position: sticky`.\n");
586 return ExamineResult::Exclude
;
589 // The frame for a <br> element has a non-zero area, but Blink treats them
590 // as if they have no area, so exclude them specially.
591 if (aFrame
->IsBrFrame()) {
592 ANCHOR_LOG("\t\tExcluding <br>.\n");
593 return ExamineResult::Exclude
;
596 // Exclude frames that aren't accessible to content.
598 aFrame
->GetContent() && aFrame
->GetContent()->ChromeOnlyAccess();
599 bool isPseudo
= aFrame
->Style()->IsPseudoElement();
600 if (isChrome
&& !isPseudo
) {
601 ANCHOR_LOG("\t\tExcluding chrome only content.\n");
602 return ExamineResult::Exclude
;
605 const bool isReplaced
= aFrame
->IsReplaced();
606 const bool isNonReplacedInline
=
607 aFrame
->StyleDisplay()->IsInlineInsideStyle() && !isReplaced
;
609 const bool isAnonBox
= aFrame
->Style()->IsAnonBox();
611 // See if this frame has or could maintain its own anchor node.
612 const bool isScrollableWithAnchor
= [&] {
613 nsIScrollableFrame
* scrollable
= do_QueryFrame(aFrame
);
617 auto* anchor
= scrollable
->Anchor();
618 return anchor
->AnchorNode() || anchor
->CanMaintainAnchor();
621 // We don't allow scroll anchors to be selected inside of nested scrollable
622 // frames which maintain an anchor node as it's not clear how an anchor
623 // adjustment should apply to multiple scrollable frames.
625 // It is important to descend into _some_ scrollable frames, specially
626 // overflow: hidden, as those don't generally maintain their own anchors, and
627 // it is a common case in the wild where scroll anchoring ought to work.
629 // We also don't allow scroll anchors to be selected inside of replaced
630 // elements (like <img>, <video>, <svg>...) as they behave atomically. SVG
631 // uses a different layout model than CSS, and the specification doesn't say
632 // it should apply anyway.
634 // [1] https://github.com/w3c/csswg-drafts/issues/3477
635 const bool canDescend
= !isScrollableWithAnchor
&& !isReplaced
;
637 // Non-replaced inline boxes (including ruby frames) and anon boxes are not
638 // acceptable anchors, so we descend if possible, or otherwise exclude them
640 if (!isText
&& (isNonReplacedInline
|| isAnonBox
)) {
642 "\t\tSearching descendants of anon or non-replaced inline box (a=%d, "
644 isAnonBox
, isNonReplacedInline
);
646 return ExamineResult::PassThrough
;
648 return ExamineResult::Exclude
;
651 // Find the scroll anchoring bounding rect.
652 nsRect rect
= FindScrollAnchoringBoundingRect(Frame(), aFrame
);
653 ANCHOR_LOG("\t\trect = [%d %d x %d %d].\n", rect
.x
, rect
.y
, rect
.width
,
656 // Check if this frame is visible in the scroll port. This will exclude rects
657 // with zero sized area. The specification is ambiguous about this [1], but
658 // this matches Blink's implementation.
660 // [1] https://github.com/w3c/csswg-drafts/issues/3483
662 if (!visibleRect
.IntersectRect(rect
,
663 Frame()->GetVisualOptimalViewingRect())) {
664 return ExamineResult::Exclude
;
667 // It's not clear what the scroll anchoring bounding rect is, for elements
668 // fragmented in the block direction (e.g. across column or page breaks).
670 // Inline-fragmented elements other than text shouldn't get here because of
671 // the isNonReplacedInline check.
673 // For text nodes that are fragmented, it's specified that we need to consider
674 // the union of its line boxes.
676 // So for text nodes we handle them by including the union of line boxes in
677 // the bounding rect of the primary frame, and not selecting any
680 // For block-outside elements we choose to consider the bounding rect of each
681 // frame individually, allowing ourselves to descend into any frame, but only
682 // selecting a frame if it's not a continuation.
683 if (canDescend
&& isContinuation
) {
684 ANCHOR_LOG("\t\tSearching descendants of a continuation.\n");
685 return ExamineResult::PassThrough
;
688 // If this frame is fully visible, then select it as the scroll anchor.
689 if (visibleRect
.IsEqualEdges(rect
)) {
690 ANCHOR_LOG("\t\tFully visible, taking.\n");
691 return ExamineResult::Accept
;
694 // If we can't descend into this frame, then select it as the scroll anchor.
696 ANCHOR_LOG("\t\tIntersects a frame that we can't descend into, taking.\n");
697 return ExamineResult::Accept
;
700 // It must be partially visible and we can descend into this frame. Examine
701 // its children for a better scroll anchor or fall back to this one.
702 ANCHOR_LOG("\t\tIntersects valid candidate, checking descendants.\n");
703 return ExamineResult::Traverse
;
706 nsIFrame
* ScrollAnchorContainer::FindAnchorIn(nsIFrame
* aFrame
) const {
707 // Visit the child lists of this frame
708 for (const auto& [list
, listID
] : aFrame
->ChildLists()) {
709 // Skip child lists that contain out-of-flow frames, we'll visit them by
710 // following placeholders in the in-flow lists so that we visit these
711 // frames in DOM order.
712 // XXX do we actually need to exclude FrameChildListID::OverflowOutOfFlow
714 if (listID
== FrameChildListID::Absolute
||
715 listID
== FrameChildListID::Fixed
||
716 listID
== FrameChildListID::Float
||
717 listID
== FrameChildListID::OverflowOutOfFlow
) {
721 // Search the child list, and return if we selected an anchor
722 if (nsIFrame
* anchor
= FindAnchorInList(list
)) {
727 // The spec requires us to do an extra pass to visit absolutely positioned
728 // frames a second time after all the children of their containing block have
731 // It's not clear why this is needed [1], but it matches Blink's
732 // implementation, and is needed for a WPT test.
734 // [1] https://github.com/w3c/csswg-drafts/issues/3465
735 const nsFrameList
& absPosList
=
736 aFrame
->GetChildList(FrameChildListID::Absolute
);
737 if (nsIFrame
* anchor
= FindAnchorInList(absPosList
)) {
744 nsIFrame
* ScrollAnchorContainer::FindAnchorInList(
745 const nsFrameList
& aFrameList
) const {
746 for (nsIFrame
* child
: aFrameList
) {
747 // If this is a placeholder, try to follow it to the out of flow frame.
748 nsIFrame
* realFrame
= nsPlaceholderFrame::GetRealFrameFor(child
);
749 if (child
!= realFrame
) {
750 // If the out of flow frame is not a descendant of our scroll frame,
751 // then it must have a different containing block and cannot be an
753 if (!nsLayoutUtils::IsProperAncestorFrame(Frame(), realFrame
)) {
755 "\t\tSkipping out of flow frame that is not a descendant of the "
759 ANCHOR_LOG("\t\tFollowing placeholder to out of flow frame.\n");
763 // Perform the candidate examination algorithm
764 ExamineResult examine
= ExamineAnchorCandidate(child
);
766 // See the comment before the definition of `ExamineResult` in
767 // `ScrollAnchorContainer.h` for an explanation of this behavior.
769 case ExamineResult::Exclude
: {
772 case ExamineResult::PassThrough
: {
773 nsIFrame
* candidate
= FindAnchorIn(child
);
779 case ExamineResult::Traverse
: {
780 nsIFrame
* candidate
= FindAnchorIn(child
);
786 case ExamineResult::Accept
: {
794 } // namespace mozilla::layout