Bug 1842999 - Part 25: Support testing elements are present in resizable typed arrays...
[gecko.git] / layout / generic / ScrollAnchorContainer.cpp
blob93336480f78ab03a7368fa248a9b14a6eb8de88d
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"
8 #include <cstddef>
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"
18 #include "nsIFrame.h"
19 #include "nsIFrameInlines.h"
20 #include "nsLayoutUtils.h"
21 #include "nsPlaceholderFrame.h"
23 using namespace mozilla::dom;
25 #ifdef DEBUG
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_), \
31 (anchor_) \
32 ->Frame() \
33 ->PresContext() \
34 ->Document() \
35 ->GetDocumentURI() \
36 ->GetSpecOrDefault() \
37 .get(), \
38 (anchor_)->Frame()->mIsRoot, ##__VA_ARGS__));
40 # define ANCHOR_LOG(fmt, ...) ANCHOR_LOG_WITH(this, fmt, ##__VA_ARGS__)
41 #else
42 # define ANCHOR_LOG(...)
43 # define ANCHOR_LOG_WITH(...)
44 #endif
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)
54 : mDisabled(false),
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();
66 if (!aFrame) {
67 return nullptr;
69 nsIScrollableFrame* nearest = nsLayoutUtils::GetNearestScrollableFrame(
70 aFrame, nsLayoutUtils::SCROLLABLE_SAME_DOC |
71 nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
72 if (nearest) {
73 return nearest->Anchor();
75 return nullptr;
78 nsIScrollableFrame* ScrollAnchorContainer::ScrollableFrame() const {
79 return Frame()->GetScrollTargetFrame();
82 /**
83 * Set the appropriate frame flags for a frame that has become or is no longer
84 * an anchor node.
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();
102 MOZ_ASSERT(frame,
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
105 // anchor
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].
127 * [1]
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
143 // the scroll frame.
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);
156 MOZ_ASSERT(
157 nsLayoutUtils::IsProperAncestorFrame(aScrollFrame, blockAncestor));
158 nsRect bounding;
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,
167 aScrollFrame);
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());
183 break;
185 case WritingMode::eBlockLR: {
186 overflowRect.SetBoxX(borderRect.X(), overflowRect.XMost());
187 break;
189 case WritingMode::eBlockRL: {
190 overflowRect.SetBoxX(overflowRect.X(), borderRect.XMost());
191 break;
195 nsRect transformed = nsLayoutUtils::TransformFrameRectToAncestor(
196 aCandidate, overflowRect, aScrollFrame);
197 return transformed;
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
203 * of aScrollFrame.
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()) {
217 return false;
220 // If we've been disabled due to heuristics, we don't anchor anymore.
221 if (mDisabled) {
222 return false;
225 const nsStyleDisplay& disp = *Frame()->StyleDisplay();
226 // Don't select a scroll anchor if the scroll frame has `overflow-anchor:
227 // none`.
228 if (disp.mOverflowAnchor != mozilla::StyleOverflowAnchor::Auto) {
229 return false;
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()) {
237 return false;
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()) {
246 return false;
249 return true;
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);
269 } else {
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,
279 mAnchorNode);
281 // Unset all flags for the old scroll anchor
282 if (oldAnchor) {
283 SetAnchorFlags(Frame()->mScrolledFrame, oldAnchor, false);
286 // Set all flags for the new scroll anchor
287 if (mAnchorNode) {
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);
293 } else {
294 ANCHOR_LOG("Anchor node has remained (%p).\n", mAnchorNode);
297 // Calculate the position to use for scroll adjustments
298 if (mAnchorNode) {
299 mLastAnchorOffset = FindScrollAnchoringBoundingOffset(Frame(), mAnchorNode);
300 ANCHOR_LOG("Using last anchor offset = %d.\n", mLastAnchorOffset);
301 } else {
302 mLastAnchorOffset = 0;
305 mAnchorNodeIsDirty = false;
308 void ScrollAnchorContainer::UserScrolled() {
309 if (mApplyingAnchorAdjustment) {
310 return;
312 InvalidateAnchor();
314 if (!StaticPrefs::
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
321 // heuristic.
322 return;
325 mHeuristic.Reset();
328 void ScrollAnchorContainer::DisablingHeuristic::Reset() {
329 mConsecutiveScrollAnchoringAdjustments = SaturateUint32(0);
330 mConsecutiveScrollAnchoringAdjustmentLength = 0;
331 mTimeStamp = {};
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
343 // it.
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) {
359 return false;
362 // We don't high resolution for this timestamp.
363 const auto now = TimeStamp::NowLoRes();
364 if (mConsecutiveScrollAnchoringAdjustments++ == 0) {
365 MOZ_ASSERT(mTimeStamp.IsNull());
366 mTimeStamp = now;
367 } else if (
368 const auto timeoutMs = StaticPrefs::
369 layout_css_scroll_anchoring_max_consecutive_adjustments_timeout_ms();
370 timeoutMs && (now - mTimeStamp).ToMilliseconds() > timeoutMs) {
371 Reset();
372 return false;
375 mConsecutiveScrollAnchoringAdjustmentLength = NSCoordSaturatingAdd(
376 mConsecutiveScrollAnchoringAdjustmentLength, aAdjustment);
378 uint32_t consecutiveAdjustments =
379 mConsecutiveScrollAnchoringAdjustments.value();
380 if (consecutiveAdjustments < maxConsecutiveAdjustments ||
381 consecutiveAdjustments > kAnchorCheckCountLimit) {
382 return false;
385 auto cssPixels =
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))) {
391 return false;
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",
408 arguments);
409 return true;
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
422 // the anchor chain.
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);
434 if (mAnchorNode) {
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) {
451 return;
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() ||
464 (StaticPrefs::
465 layout_css_scroll_anchoring_reset_heuristic_during_animation() &&
466 Frame()->IsProcessingScrollEvent()) ||
467 Frame()->ScrollAnimationState().contains(
468 nsIScrollableFrame::AnimationState::TriggeredByScript) ||
469 Frame()->GetScrollPosition() == nsPoint()) {
470 ANCHOR_LOG(
471 "Ignoring post-reflow (anchor=%p, dirty=%d, disabled=%d, "
472 "pendingRestoration=%d, scrollevent=%d, scriptAnimating=%d, "
473 "zeroScrollPos=%d pendingSuppression=%d, "
474 "container=%p).\n",
475 mAnchorNode, mAnchorNodeIsDirty, mDisabled,
476 Frame()->HasPendingScrollRestoration(),
477 Frame()->IsProcessingScrollEvent(),
478 Frame()->ScrollAnimationState().contains(
479 nsIScrollableFrame::AnimationState::TriggeredByScript),
480 Frame()->GetScrollPosition() == nsPoint(), mSuppressAnchorAdjustment,
481 this);
482 if (mSuppressAnchorAdjustment) {
483 mSuppressAnchorAdjustment = false;
484 InvalidateAnchor();
486 return;
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()) {
498 ANCHOR_LOG(
499 "Anchor might be suboptimal, invalidating to try finding a better "
500 "one\n");
501 InvalidateAnchor();
505 if (logicalAdjustment == 0) {
506 ANCHOR_LOG("Ignoring zero delta anchor adjustment for %p.\n", this);
507 mSuppressAnchorAdjustment = false;
508 return;
511 if (mSuppressAnchorAdjustment) {
512 ANCHOR_LOG("Applying anchor adjustment suppression for %p.\n", this);
513 mSuppressAnchorAdjustment = false;
514 InvalidateAnchor();
515 return;
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;
527 break;
529 case WritingMode::eBlockLR: {
530 physicalAdjustment.x = logicalAdjustment;
531 break;
533 case WritingMode::eBlockRL: {
534 physicalAdjustment.x = -logicalAdjustment;
535 break;
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);
560 #else
561 ANCHOR_LOG("\t\tVisiting frame=%p.\n", aFrame);
562 #endif
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.
597 bool isChrome =
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);
614 if (!scrollable) {
615 return false;
617 auto* anchor = scrollable->Anchor();
618 return anchor->AnchorNode() || anchor->CanMaintainAnchor();
619 }();
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
639 // altogether.
640 if (!isText && (isNonReplacedInline || isAnonBox)) {
641 ANCHOR_LOG(
642 "\t\tSearching descendants of anon or non-replaced inline box (a=%d, "
643 "i=%d).\n",
644 isAnonBox, isNonReplacedInline);
645 if (canDescend) {
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,
654 rect.height);
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
661 nsRect visibleRect;
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
678 // continuations.
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.
695 if (!canDescend) {
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
713 // too?
714 if (listID == FrameChildListID::Absolute ||
715 listID == FrameChildListID::Fixed ||
716 listID == FrameChildListID::Float ||
717 listID == FrameChildListID::OverflowOutOfFlow) {
718 continue;
721 // Search the child list, and return if we selected an anchor
722 if (nsIFrame* anchor = FindAnchorInList(list)) {
723 return anchor;
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
729 // been visited.
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)) {
738 return anchor;
741 return nullptr;
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
752 // anchor node.
753 if (!nsLayoutUtils::IsProperAncestorFrame(Frame(), realFrame)) {
754 ANCHOR_LOG(
755 "\t\tSkipping out of flow frame that is not a descendant of the "
756 "scroll frame.\n");
757 continue;
759 ANCHOR_LOG("\t\tFollowing placeholder to out of flow frame.\n");
760 child = realFrame;
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.
768 switch (examine) {
769 case ExamineResult::Exclude: {
770 continue;
772 case ExamineResult::PassThrough: {
773 nsIFrame* candidate = FindAnchorIn(child);
774 if (!candidate) {
775 continue;
777 return candidate;
779 case ExamineResult::Traverse: {
780 nsIFrame* candidate = FindAnchorIn(child);
781 if (!candidate) {
782 return child;
784 return candidate;
786 case ExamineResult::Accept: {
787 return child;
791 return nullptr;
794 } // namespace mozilla::layout