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