Source code
Revision control
Copy as Markdown
Other Tools
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
#include "ScrollAnchorContainer.h"
#include <cstddef>
#include "mozilla/dom/Text.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/PresShell.h"
#include "mozilla/ProfilerLabels.h"
#include "mozilla/ScrollContainerFrame.h"
#include "mozilla/StaticPrefs_layout.h"
#include "mozilla/ToString.h"
#include "nsBlockFrame.h"
#include "nsIFrame.h"
#include "nsIFrameInlines.h"
#include "nsLayoutUtils.h"
#include "nsPlaceholderFrame.h"
using namespace mozilla::dom;
#ifdef DEBUG
static mozilla::LazyLogModule sAnchorLog("scrollanchor");
# define ANCHOR_LOG_WITH(anchor_, fmt, ...) \
MOZ_LOG(sAnchorLog, LogLevel::Debug, \
("ANCHOR(%p, %s, root: %d): " fmt, (anchor_), \
(anchor_) \
->Frame() \
->PresContext() \
->Document() \
->GetDocumentURI() \
->GetSpecOrDefault() \
.get(), \
(anchor_)->Frame()->mIsRoot, ##__VA_ARGS__));
# define ANCHOR_LOG(fmt, ...) ANCHOR_LOG_WITH(this, fmt, ##__VA_ARGS__)
#else
# define ANCHOR_LOG(...)
# define ANCHOR_LOG_WITH(...)
#endif
namespace mozilla::layout {
ScrollContainerFrame* ScrollAnchorContainer::Frame() const {
return reinterpret_cast<ScrollContainerFrame*>(
((char*)this) - offsetof(ScrollContainerFrame, mAnchor));
}
ScrollAnchorContainer::ScrollAnchorContainer(ScrollContainerFrame* aScrollFrame)
: mDisabled(false),
mAnchorMightBeSubOptimal(false),
mAnchorNodeIsDirty(true),
mApplyingAnchorAdjustment(false),
mSuppressAnchorAdjustment(false) {
MOZ_ASSERT(aScrollFrame == Frame());
}
ScrollAnchorContainer::~ScrollAnchorContainer() = default;
ScrollAnchorContainer* ScrollAnchorContainer::FindFor(nsIFrame* aFrame) {
aFrame = aFrame->GetParent();
if (!aFrame) {
return nullptr;
}
ScrollContainerFrame* nearest = nsLayoutUtils::GetNearestScrollContainerFrame(
aFrame, nsLayoutUtils::SCROLLABLE_SAME_DOC |
nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
if (nearest) {
return nearest->Anchor();
}
return nullptr;
}
ScrollContainerFrame* ScrollAnchorContainer::ScrollContainer() const {
return Frame()->GetScrollTargetFrame();
}
/**
* Set the appropriate frame flags for a frame that has become or is no longer
* an anchor node.
*/
static void SetAnchorFlags(const nsIFrame* aScrolledFrame,
nsIFrame* aAnchorNode, bool aInScrollAnchorChain) {
nsIFrame* frame = aAnchorNode;
while (frame && frame != aScrolledFrame) {
// hold, but it may not in the case of reparenting-during-reflow (due to
// inline fragmentation or such). That looks fishy!
//
// We should either invalidate the anchor when reparenting any frame on the
// chain, or fix up the chain flags.
//
// MOZ_DIAGNOSTIC_ASSERT(frame->IsInScrollAnchorChain() !=
// aInScrollAnchorChain);
frame->SetInScrollAnchorChain(aInScrollAnchorChain);
frame = frame->GetParent();
}
MOZ_ASSERT(frame,
"The anchor node should be a descendant of the scrolled frame");
// If needed, invalidate the frame so that we start/stop highlighting the
// anchor
if (StaticPrefs::layout_css_scroll_anchoring_highlight()) {
for (nsIFrame* frame = aAnchorNode->FirstContinuation(); !!frame;
frame = frame->GetNextContinuation()) {
frame->InvalidateFrame();
}
}
}
/**
* Compute the scrollable overflow rect [1] of aCandidate relative to
* aScrollFrame with all transforms applied.
*
* The specification is ambiguous about what can be selected as a scroll anchor,
* which makes the scroll anchoring bounding rect partially undefined [2]. This
* code attempts to match the implementation in Blink.
*
* An additional unspecified behavior is that any scrollable overflow before the
* border start edge in the block axis of aScrollFrame should be clamped. This
* is to prevent absolutely positioned descendant elements from being able to
* trigger scroll adjustments [3].
*
* [1]
*/
static nsRect FindScrollAnchoringBoundingRect(const nsIFrame* aScrollFrame,
nsIFrame* aCandidate) {
MOZ_ASSERT(nsLayoutUtils::IsProperAncestorFrame(aScrollFrame, aCandidate));
if (!!Text::FromNodeOrNull(aCandidate->GetContent())) {
// This is a frame for a text node. The spec says we need to accumulate the
// union of all line boxes in the coordinate space of the scroll frame
// accounting for transforms.
//
// To do this, we translate and accumulate the overflow rect for each text
// continuation to the coordinate space of the nearest ancestor block
// frame. Then we transform the resulting rect into the coordinate space of
// the scroll frame.
//
// Transforms aren't allowed on non-replaced inline boxes, so we can assume
// that these text node continuations will have the same transform as their
// nearest block ancestor. And it should be faster to transform their union
// rather than individually transforming each overflow rect
//
// XXX for fragmented blocks, blockAncestor will be an ancestor only to the
// text continuations in the first block continuation. GetOffsetTo
// should continue to work, but is it correct with transforms or a
// performance hazard?
nsIFrame* blockAncestor =
nsLayoutUtils::FindNearestBlockAncestor(aCandidate);
MOZ_ASSERT(
nsLayoutUtils::IsProperAncestorFrame(aScrollFrame, blockAncestor));
nsRect bounding;
for (nsIFrame* continuation = aCandidate->FirstContinuation(); continuation;
continuation = continuation->GetNextContinuation()) {
nsRect overflowRect =
continuation->ScrollableOverflowRectRelativeToSelf();
overflowRect += continuation->GetOffsetTo(blockAncestor);
bounding = bounding.Union(overflowRect);
}
return nsLayoutUtils::TransformFrameRectToAncestor(blockAncestor, bounding,
aScrollFrame);
}
nsRect borderRect = aCandidate->GetRectRelativeToSelf();
nsRect overflowRect = aCandidate->ScrollableOverflowRectRelativeToSelf();
NS_ASSERTION(overflowRect.Contains(borderRect),
"overflow rect must include border rect, and the clamping logic "
"here depends on that");
// Clamp the scrollable overflow rect to the border start edge on the block
// axis of the scroll frame
WritingMode writingMode = aScrollFrame->GetWritingMode();
switch (writingMode.GetBlockDir()) {
case WritingMode::BlockDir::TB: {
overflowRect.SetBoxY(borderRect.Y(), overflowRect.YMost());
break;
}
case WritingMode::BlockDir::LR: {
overflowRect.SetBoxX(borderRect.X(), overflowRect.XMost());
break;
}
case WritingMode::BlockDir::RL: {
overflowRect.SetBoxX(overflowRect.X(), borderRect.XMost());
break;
}
}
nsRect transformed = nsLayoutUtils::TransformFrameRectToAncestor(
aCandidate, overflowRect, aScrollFrame);
return transformed;
}
/**
* Compute the offset between the scrollable overflow rect start edge of
* aCandidate and the scroll-port start edge of aScrollContainerFrame, in the
* block axis of aScrollContainerFrame.
*/
static nscoord FindScrollAnchoringBoundingOffset(
const ScrollContainerFrame* aScrollContainerFrame, nsIFrame* aCandidate) {
WritingMode writingMode = aScrollContainerFrame->GetWritingMode();
nsRect physicalBounding =
FindScrollAnchoringBoundingRect(aScrollContainerFrame, aCandidate);
LogicalRect logicalBounding(
writingMode, physicalBounding,
aScrollContainerFrame->GetScrolledFrame()->GetSize());
return logicalBounding.BStart(writingMode);
}
bool ScrollAnchorContainer::CanMaintainAnchor() const {
if (!StaticPrefs::layout_css_scroll_anchoring_enabled()) {
return false;
}
// If we've been disabled due to heuristics, we don't anchor anymore.
if (mDisabled) {
return false;
}
const nsStyleDisplay& disp = *Frame()->StyleDisplay();
// Don't select a scroll anchor if the scroll frame has `overflow-anchor:
// none`.
if (disp.mOverflowAnchor != mozilla::StyleOverflowAnchor::Auto) {
return false;
}
// Or if the scroll frame has not been scrolled from the logical origin of the
// block axis. This is not in the specification [1], but Blink does this [2].
//
// [2]
const nsPoint pos = Frame()->GetLogicalScrollPosition();
const nscoord blockOffset =
Frame()->GetWritingMode().IsVertical() ? pos.x : pos.y;
if (blockOffset == 0) {
return false;
}
// Or if there is perspective that could affect the scrollable overflow rect
// for descendant frames. This is not in the specification as Blink doesn't
// share this behavior with perspective [1].
//
if (Frame()->ChildrenHavePerspective()) {
return false;
}
return true;
}
void ScrollAnchorContainer::SelectAnchor() {
MOZ_ASSERT(Frame()->mScrolledFrame);
MOZ_ASSERT(mAnchorNodeIsDirty);
AUTO_PROFILER_LABEL("ScrollAnchorContainer::SelectAnchor", LAYOUT);
ANCHOR_LOG("Selecting anchor with scroll-port=%s.\n",
mozilla::ToString(Frame()->GetVisualOptimalViewingRect()).c_str());
// Select a new scroll anchor
nsIFrame* oldAnchor = mAnchorNode;
if (CanMaintainAnchor()) {
MOZ_DIAGNOSTIC_ASSERT(
!Frame()->mScrolledFrame->IsInScrollAnchorChain(),
"Our scrolled frame can't serve as or contain an anchor for an "
"ancestor if it can maintain its own anchor");
ANCHOR_LOG("Beginning selection.\n");
mAnchorNode = FindAnchorIn(Frame()->mScrolledFrame);
} else {
ANCHOR_LOG("Skipping selection, doesn't maintain a scroll anchor.\n");
mAnchorNode = nullptr;
}
mAnchorMightBeSubOptimal =
mAnchorNode && mAnchorNode->HasAnyStateBits(NS_FRAME_HAS_DIRTY_CHILDREN);
// Update the anchor flags if needed
if (oldAnchor != mAnchorNode) {
ANCHOR_LOG("Anchor node has changed from (%p) to (%p).\n", oldAnchor,
mAnchorNode);
// Unset all flags for the old scroll anchor
if (oldAnchor) {
SetAnchorFlags(Frame()->mScrolledFrame, oldAnchor, false);
}
// Set all flags for the new scroll anchor
if (mAnchorNode) {
// Anchor selection will never select a descendant of a nested scroll
// frame which maintains an anchor, so we can set flags without
// conflicting with other scroll anchor containers.
SetAnchorFlags(Frame()->mScrolledFrame, mAnchorNode, true);
}
} else {
ANCHOR_LOG("Anchor node has remained (%p).\n", mAnchorNode);
}
// Calculate the position to use for scroll adjustments
if (mAnchorNode) {
mLastAnchorOffset = FindScrollAnchoringBoundingOffset(Frame(), mAnchorNode);
ANCHOR_LOG("Using last anchor offset = %s.\n",
ToString(CSSPixel::FromAppUnits(mLastAnchorOffset)).c_str());
} else {
mLastAnchorOffset = 0;
}
mAnchorNodeIsDirty = false;
}
void ScrollAnchorContainer::UserScrolled() {
if (mApplyingAnchorAdjustment) {
return;
}
InvalidateAnchor();
if (!StaticPrefs::
layout_css_scroll_anchoring_reset_heuristic_during_animation() &&
Frame()->ScrollAnimationState().contains(
ScrollContainerFrame::AnimationState::APZInProgress)) {
// We'd want to skip resetting our heuristic while APZ is running an async
// scroll because this UserScrolled function gets called on every refresh
// driver's tick during running the async scroll, thus it will clobber the
// heuristic.
return;
}
mHeuristic.Reset();
}
void ScrollAnchorContainer::DisablingHeuristic::Reset() {
mConsecutiveScrollAnchoringAdjustments = SaturateUint32(0);
mConsecutiveScrollAnchoringAdjustmentLength = 0;
mTimeStamp = {};
}
void ScrollAnchorContainer::AdjustmentMade(nscoord aAdjustment) {
MOZ_ASSERT(!mDisabled, "How?");
mDisabled = mHeuristic.AdjustmentMade(*this, aAdjustment);
}
bool ScrollAnchorContainer::DisablingHeuristic::AdjustmentMade(
const ScrollAnchorContainer& aAnchor, nscoord aAdjustment) {
// A reasonably large number of times that we want to check for this. If we
// haven't hit this limit after these many attempts we assume we'll never hit
// it.
//
// This is to prevent the number getting too large and making the limit round
// to zero by mere precision error.
//
// 100k should be enough for anyone :)
static const uint32_t kAnchorCheckCountLimit = 100000;
// Zero-length adjustments are common & don't have side effects, so we don't
// want them to consider them here; they'd bias our average towards 0.
MOZ_ASSERT(aAdjustment, "Don't call this API for zero-length adjustments");
const uint32_t maxConsecutiveAdjustments =
StaticPrefs::layout_css_scroll_anchoring_max_consecutive_adjustments();
if (!maxConsecutiveAdjustments) {
return false;
}
// We don't high resolution for this timestamp.
const auto now = TimeStamp::NowLoRes();
if (mConsecutiveScrollAnchoringAdjustments++ == 0) {
MOZ_ASSERT(mTimeStamp.IsNull());
mTimeStamp = now;
} else if (
const auto timeoutMs = StaticPrefs::
layout_css_scroll_anchoring_max_consecutive_adjustments_timeout_ms();
timeoutMs && (now - mTimeStamp).ToMilliseconds() > timeoutMs) {
Reset();
return false;
}
mConsecutiveScrollAnchoringAdjustmentLength = NSCoordSaturatingAdd(
mConsecutiveScrollAnchoringAdjustmentLength, aAdjustment);
uint32_t consecutiveAdjustments =
mConsecutiveScrollAnchoringAdjustments.value();
if (consecutiveAdjustments < maxConsecutiveAdjustments ||
consecutiveAdjustments > kAnchorCheckCountLimit) {
return false;
}
auto cssPixels =
CSSPixel::FromAppUnits(mConsecutiveScrollAnchoringAdjustmentLength);
double average = double(cssPixels) / consecutiveAdjustments;
uint32_t minAverage = StaticPrefs::
layout_css_scroll_anchoring_min_average_adjustment_threshold();
if (MOZ_LIKELY(std::abs(average) >= double(minAverage))) {
return false;
}
ANCHOR_LOG_WITH(&aAnchor,
"Disabled scroll anchoring for container: "
"%f average, %f total out of %u consecutive adjustments\n",
average, float(cssPixels), consecutiveAdjustments);
AutoTArray<nsString, 3> arguments;
arguments.AppendElement()->AppendInt(consecutiveAdjustments);
arguments.AppendElement()->AppendFloat(average);
arguments.AppendElement()->AppendFloat(cssPixels);
nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "Layout"_ns,
aAnchor.Frame()->PresContext()->Document(),
nsContentUtils::eLAYOUT_PROPERTIES,
"ScrollAnchoringDisabledInContainer",
arguments);
return true;
}
void ScrollAnchorContainer::SuppressAdjustments() {
ANCHOR_LOG("Received a scroll anchor suppression for %p.\n", this);
mSuppressAnchorAdjustment = true;
// Forward to our parent if appropriate, that is, if we don't maintain an
// anchor, and we can't maintain one.
//
// Note that we need to check !CanMaintainAnchor(), instead of just whether
// our frame is in the anchor chain of our ancestor as InvalidateAnchor()
// does, given some suppression triggers apply even for nodes that are not in
// the anchor chain.
if (!mAnchorNode && !CanMaintainAnchor()) {
if (ScrollAnchorContainer* container = FindFor(Frame())) {
ANCHOR_LOG(" > Forwarding to parent anchor\n");
container->SuppressAdjustments();
}
}
}
void ScrollAnchorContainer::InvalidateAnchor(ScheduleSelection aSchedule) {
ANCHOR_LOG("Invalidating scroll anchor %p for %p.\n", mAnchorNode, this);
if (mAnchorNode) {
SetAnchorFlags(Frame()->mScrolledFrame, mAnchorNode, false);
} else if (Frame()->mScrolledFrame->IsInScrollAnchorChain()) {
ANCHOR_LOG(" > Forwarding to parent anchor\n");
// We don't maintain an anchor, and our scrolled frame is in the anchor
// chain of an ancestor. Invalidate that anchor.
//
// NOTE: Intentionally not forwarding aSchedule: Scheduling is always safe
// and not doing so is just an optimization.
FindFor(Frame())->InvalidateAnchor();
}
mAnchorNode = nullptr;
mAnchorMightBeSubOptimal = false;
mAnchorNodeIsDirty = true;
mLastAnchorOffset = 0;
if (!CanMaintainAnchor() || aSchedule == ScheduleSelection::No) {
return;
}
Frame()->PresShell()->PostPendingScrollAnchorSelection(this);
}
void ScrollAnchorContainer::Destroy() {
InvalidateAnchor(ScheduleSelection::No);
}
void ScrollAnchorContainer::ApplyAdjustments() {
if (!mAnchorNode || mAnchorNodeIsDirty || mDisabled ||
Frame()->HasPendingScrollRestoration() ||
(StaticPrefs::
layout_css_scroll_anchoring_reset_heuristic_during_animation() &&
Frame()->IsProcessingScrollEvent()) ||
Frame()->ScrollAnimationState().contains(
ScrollContainerFrame::AnimationState::TriggeredByScript) ||
Frame()->GetScrollPosition() == nsPoint()) {
ANCHOR_LOG(
"Ignoring post-reflow (anchor=%p, dirty=%d, disabled=%d, "
"pendingRestoration=%d, scrollevent=%d, scriptAnimating=%d, "
"zeroScrollPos=%d pendingSuppression=%d, "
"container=%p).\n",
mAnchorNode, mAnchorNodeIsDirty, mDisabled,
Frame()->HasPendingScrollRestoration(),
Frame()->IsProcessingScrollEvent(),
Frame()->ScrollAnimationState().contains(
ScrollContainerFrame::AnimationState::TriggeredByScript),
Frame()->GetScrollPosition() == nsPoint(), mSuppressAnchorAdjustment,
this);
if (mSuppressAnchorAdjustment) {
mSuppressAnchorAdjustment = false;
InvalidateAnchor();
}
return;
}
nscoord current = FindScrollAnchoringBoundingOffset(Frame(), mAnchorNode);
nscoord logicalAdjustment = current - mLastAnchorOffset;
WritingMode writingMode = Frame()->GetWritingMode();
ANCHOR_LOG("Anchor has moved from %s to %s.\n",
ToString(CSSPixel::FromAppUnits(mLastAnchorOffset)).c_str(),
ToString(CSSPixel::FromAppUnits(current)).c_str());
auto maybeInvalidate = MakeScopeExit([&] {
if (mAnchorMightBeSubOptimal &&
StaticPrefs::layout_css_scroll_anchoring_reselect_if_suboptimal()) {
ANCHOR_LOG(
"Anchor might be suboptimal, invalidating to try finding a better "
"one\n");
InvalidateAnchor();
}
});
if (logicalAdjustment == 0) {
ANCHOR_LOG("Ignoring zero delta anchor adjustment for %p.\n", this);
mSuppressAnchorAdjustment = false;
return;
}
if (mSuppressAnchorAdjustment) {
ANCHOR_LOG("Applying anchor adjustment suppression for %p.\n", this);
mSuppressAnchorAdjustment = false;
InvalidateAnchor();
return;
}
ANCHOR_LOG("Applying anchor adjustment of %s in %s with anchor %p.\n",
ToString(CSSPixel::FromAppUnits(logicalAdjustment)).c_str(),
ToString(writingMode).c_str(), mAnchorNode);
AdjustmentMade(logicalAdjustment);
nsPoint physicalAdjustment;
switch (writingMode.GetBlockDir()) {
case WritingMode::BlockDir::TB: {
physicalAdjustment.y = logicalAdjustment;
break;
}
case WritingMode::BlockDir::LR: {
physicalAdjustment.x = logicalAdjustment;
break;
}
case WritingMode::BlockDir::RL: {
physicalAdjustment.x = -logicalAdjustment;
break;
}
}
MOZ_RELEASE_ASSERT(!mApplyingAnchorAdjustment);
// We should use AutoRestore here, but that doesn't work with bitfields
mApplyingAnchorAdjustment = true;
Frame()->ScrollToInternal(Frame()->GetScrollPosition() + physicalAdjustment,
ScrollMode::Instant, ScrollOrigin::Relative);
mApplyingAnchorAdjustment = false;
if (Frame()->mIsRoot) {
Frame()->PresShell()->RootScrollFrameAdjusted(physicalAdjustment.y);
}
// The anchor position may not be in the same relative position after
// adjustment. Update ourselves so we have consistent state.
mLastAnchorOffset = FindScrollAnchoringBoundingOffset(Frame(), mAnchorNode);
}
ScrollAnchorContainer::ExamineResult
ScrollAnchorContainer::ExamineAnchorCandidate(nsIFrame* aFrame) const {
#ifdef DEBUG_FRAME_DUMP
nsCString tag = aFrame->ListTag();
ANCHOR_LOG("\tVisiting frame=%s (%p).\n", tag.get(), aFrame);
#else
ANCHOR_LOG("\t\tVisiting frame=%p.\n", aFrame);
#endif
bool isText = !!Text::FromNodeOrNull(aFrame->GetContent());
bool isContinuation = !!aFrame->GetPrevContinuation();
if (isText && isContinuation) {
ANCHOR_LOG("\t\tExcluding continuation text node.\n");
return ExamineResult::Exclude;
}
// Check if the author has opted out of scroll anchoring for this frame
// and its descendants.
const nsStyleDisplay* disp = aFrame->StyleDisplay();
if (disp->mOverflowAnchor == mozilla::StyleOverflowAnchor::None) {
ANCHOR_LOG("\t\tExcluding `overflow-anchor: none`.\n");
return ExamineResult::Exclude;
}
// Sticky positioned elements can move with the scroll frame, making them
// unsuitable scroll anchors. This isn't in the specification yet [1], but
// matches Blink's implementation.
//
if (aFrame->IsStickyPositioned()) {
ANCHOR_LOG("\t\tExcluding `position: sticky`.\n");
return ExamineResult::Exclude;
}
// The frame for a <br> element has a non-zero area, but Blink treats them
// as if they have no area, so exclude them specially.
if (aFrame->IsBrFrame()) {
ANCHOR_LOG("\t\tExcluding <br>.\n");
return ExamineResult::Exclude;
}
// Exclude frames that aren't accessible to content.
bool isChrome =
aFrame->GetContent() && aFrame->GetContent()->ChromeOnlyAccess();
bool isPseudo = aFrame->Style()->IsPseudoElement();
if (isChrome && !isPseudo) {
ANCHOR_LOG("\t\tExcluding chrome only content.\n");
return ExamineResult::Exclude;
}
const bool isReplaced = aFrame->IsReplaced();
const bool isNonReplacedInline =
aFrame->StyleDisplay()->IsInlineInsideStyle() && !isReplaced;
const bool isAnonBox = aFrame->Style()->IsAnonBox();
// See if this frame has or could maintain its own anchor node.
const bool isScrollableWithAnchor = [&] {
ScrollContainerFrame* scrollContainer = do_QueryFrame(aFrame);
if (!scrollContainer) {
return false;
}
auto* anchor = scrollContainer->Anchor();
return anchor->AnchorNode() || anchor->CanMaintainAnchor();
}();
// We don't allow scroll anchors to be selected inside of nested scrollable
// frames which maintain an anchor node as it's not clear how an anchor
// adjustment should apply to multiple scrollable frames.
//
// It is important to descend into _some_ scrollable frames, specially
// overflow: hidden, as those don't generally maintain their own anchors, and
// it is a common case in the wild where scroll anchoring ought to work.
//
// We also don't allow scroll anchors to be selected inside of replaced
// elements (like <img>, <video>, <svg>...) as they behave atomically. SVG
// uses a different layout model than CSS, and the specification doesn't say
// it should apply anyway.
//
const bool canDescend = !isScrollableWithAnchor && !isReplaced;
// Non-replaced inline boxes (including ruby frames) and anon boxes are not
// acceptable anchors, so we descend if possible, or otherwise exclude them
// altogether.
if (!isText && (isNonReplacedInline || isAnonBox)) {
ANCHOR_LOG(
"\t\tSearching descendants of anon or non-replaced inline box (a=%d, "
"i=%d).\n",
isAnonBox, isNonReplacedInline);
if (canDescend) {
return ExamineResult::PassThrough;
}
return ExamineResult::Exclude;
}
// Find the scroll anchoring bounding rect.
nsRect rect = FindScrollAnchoringBoundingRect(Frame(), aFrame);
ANCHOR_LOG("\t\trect = %s.\n", ToString(CSSRect::FromAppUnits(rect)).c_str());
// Check if this frame is visible in the scroll port. This will exclude rects
// with zero sized area. The specification is ambiguous about this [1], but
// this matches Blink's implementation.
//
nsRect visibleRect;
if (!visibleRect.IntersectRect(rect,
Frame()->GetVisualOptimalViewingRect())) {
return ExamineResult::Exclude;
}
// It's not clear what the scroll anchoring bounding rect is, for elements
// fragmented in the block direction (e.g. across column or page breaks).
//
// Inline-fragmented elements other than text shouldn't get here because of
// the isNonReplacedInline check.
//
// For text nodes that are fragmented, it's specified that we need to consider
// the union of its line boxes.
//
// So for text nodes we handle them by including the union of line boxes in
// the bounding rect of the primary frame, and not selecting any
// continuations.
//
// For block-outside elements we choose to consider the bounding rect of each
// frame individually, allowing ourselves to descend into any frame, but only
// selecting a frame if it's not a continuation.
if (canDescend && isContinuation) {
ANCHOR_LOG("\t\tSearching descendants of a continuation.\n");
return ExamineResult::PassThrough;
}
// If this frame is fully visible, then select it as the scroll anchor.
if (visibleRect.IsEqualEdges(rect)) {
ANCHOR_LOG("\t\tFully visible, taking.\n");
return ExamineResult::Accept;
}
// If we can't descend into this frame, then select it as the scroll anchor.
if (!canDescend) {
ANCHOR_LOG("\t\tIntersects a frame that we can't descend into, taking.\n");
return ExamineResult::Accept;
}
// It must be partially visible and we can descend into this frame. Examine
// its children for a better scroll anchor or fall back to this one.
ANCHOR_LOG("\t\tIntersects valid candidate, checking descendants.\n");
return ExamineResult::Traverse;
}
nsIFrame* ScrollAnchorContainer::FindAnchorIn(nsIFrame* aFrame) const {
// Visit the child lists of this frame
for (const auto& [list, listID] : aFrame->ChildLists()) {
// Skip child lists that contain out-of-flow frames, we'll visit them by
// following placeholders in the in-flow lists so that we visit these
// frames in DOM order.
// XXX do we actually need to exclude FrameChildListID::OverflowOutOfFlow
// too?
if (listID == FrameChildListID::Absolute ||
listID == FrameChildListID::Fixed ||
listID == FrameChildListID::Float ||
listID == FrameChildListID::OverflowOutOfFlow) {
continue;
}
// Search the child list, and return if we selected an anchor
if (nsIFrame* anchor = FindAnchorInList(list)) {
return anchor;
}
}
// The spec requires us to do an extra pass to visit absolutely positioned
// frames a second time after all the children of their containing block have
// been visited.
//
// It's not clear why this is needed [1], but it matches Blink's
// implementation, and is needed for a WPT test.
//
const nsFrameList& absPosList =
aFrame->GetChildList(FrameChildListID::Absolute);
if (nsIFrame* anchor = FindAnchorInList(absPosList)) {
return anchor;
}
return nullptr;
}
nsIFrame* ScrollAnchorContainer::FindAnchorInList(
const nsFrameList& aFrameList) const {
for (nsIFrame* child : aFrameList) {
// If this is a placeholder, try to follow it to the out of flow frame.
nsIFrame* realFrame = nsPlaceholderFrame::GetRealFrameFor(child);
if (child != realFrame) {
// If the out of flow frame is not a descendant of our scroll frame,
// then it must have a different containing block and cannot be an
// anchor node.
if (!nsLayoutUtils::IsProperAncestorFrame(Frame(), realFrame)) {
ANCHOR_LOG(
"\t\tSkipping out of flow frame that is not a descendant of the "
"scroll frame.\n");
continue;
}
ANCHOR_LOG("\t\tFollowing placeholder to out of flow frame.\n");
child = realFrame;
}
// Perform the candidate examination algorithm
ExamineResult examine = ExamineAnchorCandidate(child);
// See the comment before the definition of `ExamineResult` in
// `ScrollAnchorContainer.h` for an explanation of this behavior.
switch (examine) {
case ExamineResult::Exclude: {
continue;
}
case ExamineResult::PassThrough: {
nsIFrame* candidate = FindAnchorIn(child);
if (!candidate) {
continue;
}
return candidate;
}
case ExamineResult::Traverse: {
nsIFrame* candidate = FindAnchorIn(child);
if (!candidate) {
return child;
}
return candidate;
}
case ExamineResult::Accept: {
return child;
}
}
}
return nullptr;
}
} // namespace mozilla::layout