diff --git a/layout/base/nsLayoutUtils.cpp b/layout/base/nsLayoutUtils.cpp index 53d54979ee39..780d86fcf61c 100644 --- a/layout/base/nsLayoutUtils.cpp +++ b/layout/base/nsLayoutUtils.cpp @@ -1490,11 +1490,11 @@ bool nsLayoutUtils::IsAncestorFrameCrossDoc(const nsIFrame* aAncestorFrame, } // static -bool nsLayoutUtils::IsProperAncestorFrame(nsIFrame* aAncestorFrame, - nsIFrame* aFrame, - nsIFrame* aCommonAncestor) { +bool nsLayoutUtils::IsProperAncestorFrame(const nsIFrame* aAncestorFrame, + const nsIFrame* aFrame, + const nsIFrame* aCommonAncestor) { if (aFrame == aAncestorFrame) return false; - for (nsIFrame* f = aFrame; f != aCommonAncestor; f = f->GetParent()) { + for (const nsIFrame* f = aFrame; f != aCommonAncestor; f = f->GetParent()) { if (f == aAncestorFrame) return true; } return aCommonAncestor == aAncestorFrame; diff --git a/layout/base/nsLayoutUtils.h b/layout/base/nsLayoutUtils.h index 11c1d798a264..f10265c58e60 100644 --- a/layout/base/nsLayoutUtils.h +++ b/layout/base/nsLayoutUtils.h @@ -528,8 +528,9 @@ class nsLayoutUtils { * aAncestorFrame. If non-null, this can bound the search and speed up * the function */ - static bool IsProperAncestorFrame(nsIFrame* aAncestorFrame, nsIFrame* aFrame, - nsIFrame* aCommonAncestor = nullptr); + static bool IsProperAncestorFrame(const nsIFrame* aAncestorFrame, + const nsIFrame* aFrame, + const nsIFrame* aCommonAncestor = nullptr); /** * Like IsProperAncestorFrame, but looks across document boundaries. diff --git a/layout/generic/ScrollAnchorContainer.cpp b/layout/generic/ScrollAnchorContainer.cpp index 9e3be14465e8..5fb1ff3bc413 100644 --- a/layout/generic/ScrollAnchorContainer.cpp +++ b/layout/generic/ScrollAnchorContainer.cpp @@ -6,6 +6,7 @@ #include "ScrollAnchorContainer.h" +#include "mozilla/StaticPrefs.h" #include "nsGfxScrollFrame.h" #include "nsLayoutUtils.h" @@ -16,7 +17,10 @@ namespace mozilla { namespace layout { ScrollAnchorContainer::ScrollAnchorContainer(ScrollFrameHelper* aScrollFrame) - : mScrollFrame(aScrollFrame) {} + : mScrollFrame(aScrollFrame), + mAnchorNode(nullptr), + mLastAnchorPos(0, 0), + mAnchorNodeIsDirty(true) {} ScrollAnchorContainer::~ScrollAnchorContainer() {} @@ -40,5 +44,393 @@ nsIScrollableFrame* ScrollAnchorContainer::ScrollableFrame() 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) { + MOZ_ASSERT( + frame == aAnchorNode || !frame->IsScrollFrame(), + "We shouldn't select an anchor node inside a nested scroll frame."); + + frame->SetInScrollAnchorChain(aInScrollAnchorChain); + frame = frame->GetParent(); + } + MOZ_ASSERT(frame, + "The anchor node should be a descendant of the scroll frame"); +} + +/** + * Compute the scrollable overflow rect [1] of aCandidate relative to + * aScrollFrame with all transforms applied. The specification is also + * 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. + * + * [1] + * https://drafts.csswg.org/css-scroll-anchoring-1/#scroll-anchoring-bounding-rect + * [2] https://github.com/w3c/csswg-drafts/issues/3478 + */ +static nsRect FindScrollAnchoringBoundingRect(const nsIFrame* aScrollFrame, + nsIFrame* aCandidate) { + MOZ_ASSERT(nsLayoutUtils::IsProperAncestorFrame(aScrollFrame, aCandidate)); + if (aCandidate->GetContent()->IsText()) { + nsRect bounding; + for (nsIFrame* continuation = aCandidate->FirstContinuation(); continuation; + continuation = continuation->GetNextContinuation()) { + nsRect localRect = + continuation->GetScrollableOverflowRectRelativeToSelf(); + nsRect transformed = nsLayoutUtils::TransformFrameRectToAncestor( + continuation, localRect, aScrollFrame); + bounding = bounding.Union(transformed); + } + return bounding; + } + + nsRect localRect = aCandidate->GetScrollableOverflowRectRelativeToSelf(); + nsRect transformed = nsLayoutUtils::TransformFrameRectToAncestor( + aCandidate, localRect, aScrollFrame); + return transformed; +} + +void ScrollAnchorContainer::SelectAnchor() { + MOZ_ASSERT(mScrollFrame->mScrolledFrame); + MOZ_ASSERT(mAnchorNodeIsDirty); + + if (!StaticPrefs::layout_css_scroll_anchoring_enabled()) { + return; + } + + ANCHOR_LOG("Selecting anchor for %p with scroll-port [%d %d x %d %d].\n", + this, mScrollFrame->mScrollPort.x, mScrollFrame->mScrollPort.y, + mScrollFrame->mScrollPort.width, mScrollFrame->mScrollPort.height); + + const nsStyleDisplay* disp = Frame()->StyleDisplay(); + + // Don't select a scroll anchor if the scroll frame has `overflow-anchor: + // none`. + bool overflowAnchor = + disp->mOverflowAnchor == mozilla::StyleOverflowAnchor::Auto; + + // Or if the scroll frame has not been scrolled from the logical origin. This + // is not in the specification [1], but Blink does this. + // + // [1] https://github.com/w3c/csswg-drafts/issues/3319 + bool isScrolled = mScrollFrame->GetLogicalScrollPosition() != nsPoint(); + + // 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]. + // + // [1] https://github.com/w3c/csswg-drafts/issues/3322 + bool hasPerspective = Frame()->ChildrenHavePerspective(); + + // Select a new scroll anchor + nsIFrame* oldAnchor = mAnchorNode; + if (overflowAnchor && isScrolled && !hasPerspective) { + ANCHOR_LOG("Beginning candidate selection.\n"); + mAnchorNode = FindAnchorIn(mScrollFrame->mScrolledFrame); + } else { + if (!overflowAnchor) { + ANCHOR_LOG("Skipping candidate selection for `overflow-anchor: none`\n"); + } + if (!isScrolled) { + ANCHOR_LOG("Skipping candidate selection for not being scrolled\n"); + } + if (hasPerspective) { + ANCHOR_LOG( + "Skipping candidate selection for scroll frame with perspective\n"); + } + mAnchorNode = nullptr; + } + + // 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(mScrollFrame->mScrolledFrame, oldAnchor, false); + } + + // Set all flags for the new scroll anchor + if (mAnchorNode) { + // Anchor selection will never select a descendant of a different scroll + // frame, so we can set flags without conflicting with other scroll + // anchor containers. + SetAnchorFlags(mScrollFrame->mScrolledFrame, mAnchorNode, true); + } + } else { + ANCHOR_LOG("Anchor node has remained (%p).\n", mAnchorNode); + } + + // Calculate the position to use for scroll adjustments + if (mAnchorNode) { + mLastAnchorPos = + FindScrollAnchoringBoundingRect(Frame(), mAnchorNode).TopLeft(); + ANCHOR_LOG("Using last anchor position = [%d, %d].\n", mLastAnchorPos.x, + mLastAnchorPos.y); + } else { + mLastAnchorPos = nsPoint(); + } + + mAnchorNodeIsDirty = false; +} + +void ScrollAnchorContainer::UserScrolled() { InvalidateAnchor(); } + +void ScrollAnchorContainer::InvalidateAnchor() { + if (!StaticPrefs::layout_css_scroll_anchoring_enabled()) { + return; + } + + ANCHOR_LOG("Invalidating scroll anchor %p for %p.\n", mAnchorNode, this); + + if (mAnchorNode) { + SetAnchorFlags(mScrollFrame->mScrolledFrame, mAnchorNode, false); + } + mAnchorNode = nullptr; + mAnchorNodeIsDirty = true; + mLastAnchorPos = nsPoint(); +} + +void ScrollAnchorContainer::Destroy() { + if (mAnchorNode) { + SetAnchorFlags(mScrollFrame->mScrolledFrame, mAnchorNode, false); + } + mAnchorNode = nullptr; + mAnchorNodeIsDirty = false; + mLastAnchorPos = nsPoint(); +} + +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 + + // 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. + // + // [1] https://github.com/w3c/csswg-drafts/issues/3319 + if (aFrame->IsStickyPositioned()) { + ANCHOR_LOG("\t\tExcluding `position: sticky`.\n"); + return ExamineResult::Exclude; + } + + // The frame for a
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
.\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; + } + + // See if this frame could have its own anchor node. We could check + // IsScrollFrame(), but that would miss nsListControlFrame which is not a + // scroll frame, but still inherits from nsHTMLScrollFrame. + nsIScrollableFrame* scrollable = do_QueryFrame(aFrame); + + // We don't allow scroll anchors to be selected inside of scrollable frames as + // it's not clear how an anchor adjustment should apply to multiple scrollable + // frames. Blink allows this to happen, but they're not sure why [1]. + // + // We also don't allow scroll anchors to be selected inside of SVG as it uses + // a different layout model than CSS, and the specification doesn't say it + // should apply. + // + // [1] https://github.com/w3c/csswg-drafts/issues/3477 + bool canDescend = !scrollable && !aFrame->IsSVGOuterSVGFrame(); + + // Check what kind of frame this is + bool isBlockOutside = aFrame->IsBlockOutside(); + bool isText = aFrame->GetContent()->IsText(); + bool isAnonBox = aFrame->Style()->IsAnonBox() && !isText; + bool isInlineOutside = aFrame->IsInlineOutside() && !isText; + bool isContinuation = !!aFrame->GetPrevContinuation(); + + // If the frame is anonymous or inline-outside, search its descendants for a + // scroll anchor. + if ((isAnonBox || isInlineOutside) && canDescend) { + ANCHOR_LOG( + "\t\tSearching descendants of anon or inline box (a=%d, i=%d).\n", + isAnonBox, isInlineOutside); + return ExamineResult::PassThrough; + } + + // If the frame is not block-outside or a text node then exclude it. + if (!isBlockOutside && !isText) { + ANCHOR_LOG("\t\tExcluding non block-outside or text node (b=%d, t=%d).\n", + isBlockOutside, isText); + return ExamineResult::Exclude; + } + + // Find the scroll anchoring bounding rect. + nsRect rect = FindScrollAnchoringBoundingRect(Frame(), aFrame); + ANCHOR_LOG("\t\trect = [%d %d x %d %d].\n", rect.x, rect.y, rect.width, + rect.height); + + // 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. + // + // [1] https://github.com/w3c/csswg-drafts/issues/3483 + nsRect visibleRect; + if (!visibleRect.IntersectRect(rect, mScrollFrame->mScrollPort)) { + return ExamineResult::Exclude; + } + + // At this point, if canDescend is true, we should only have visible + // non-anonymous frames that are either: + // 1. block-outside + // 2. text nodes + // + // It's not clear what the scroll anchoring bounding rect of elements that are + // block-outside should be when they are fragmented. 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 (nsIFrame::ChildListIterator lists(aFrame); !lists.IsDone(); + lists.Next()) { + // 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 kOverflowOutOfFlowList too? + if (lists.CurrentID() == FrameChildListID::kAbsoluteList || + lists.CurrentID() == FrameChildListID::kFixedList || + lists.CurrentID() == FrameChildListID::kFloatList || + lists.CurrentID() == FrameChildListID::kOverflowOutOfFlowList) { + continue; + } + + // Search the child list, and return if we selected an anchor + if (nsIFrame* anchor = FindAnchorInList(lists.CurrentList())) { + 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. + // + // [1] https://github.com/w3c/csswg-drafts/issues/3465 + const nsFrameList& absPosList = + aFrame->GetChildList(FrameChildListID::kAbsoluteList); + 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 layout } // namespace mozilla diff --git a/layout/generic/ScrollAnchorContainer.h b/layout/generic/ScrollAnchorContainer.h index e1805b8f44a4..142838215cd4 100644 --- a/layout/generic/ScrollAnchorContainer.h +++ b/layout/generic/ScrollAnchorContainer.h @@ -7,6 +7,9 @@ #ifndef mozilla_layout_ScrollAnchorContainer_h_ #define mozilla_layout_ScrollAnchorContainer_h_ +#include "nsPoint.h" + +class nsIFrame; namespace mozilla { class ScrollFrameHelper; } // namespace mozilla @@ -32,6 +35,12 @@ class ScrollAnchorContainer final { */ static ScrollAnchorContainer* FindFor(nsIFrame* aFrame); + /** + * Returns the frame that is the selected anchor node or null if no anchor + * is selected. + */ + nsIFrame* AnchorNode() const { return mAnchorNode; } + /** * Returns the frame that owns this scroll anchor container. This is always * non-null. @@ -44,9 +53,80 @@ class ScrollAnchorContainer final { */ nsIScrollableFrame* ScrollableFrame() const; + /** + * Find a suitable anchor node among the descendants of the scrollable frame. + * This should only be called after the scroll anchor has been invalidated. + */ + void SelectAnchor(); + + /** + * Notify the scroll anchor container that its scroll frame has been + * scrolled by a user and should invalidate itself. + */ + void UserScrolled(); + + /** + * Notify this scroll anchor container that its anchor node should be + * invalidated and recomputed at the next available opportunity. + */ + void InvalidateAnchor(); + + /** + * Notify this scroll anchor container that it will be destroyed along with + * its parent frame. + */ + void Destroy(); + private: + // Represents an assessment of a frame's suitability as a scroll anchor, + // from the scroll-anchoring spec's "candidate examination algorithm": + // https://drafts.csswg.org/css-scroll-anchoring-1/#candidate-examination + enum class ExamineResult { + // The frame is an excluded subtree or fully clipped and should be ignored. + // This corresponds with step 1 in the algorithm. + Exclude, + // This frame is an anonymous or inline box and its descendants should be + // searched to find an anchor node. If none are found, then continue + // searching. This is implied by the prologue of the algorithm, and + // should be made explicit in the spec [1]. + // + // [1] https://github.com/w3c/csswg-drafts/issues/3489 + PassThrough, + // The frame is partially visible and its descendants should be searched to + // find an anchor node. If none are found then this frame should be + // selected. This corresponds with step 3 in the algorithm. + Traverse, + // The frame is fully visible and should be selected as an anchor node. This + // corresponds with step 2 in the algorithm. + Accept, + }; + + ExamineResult ExamineAnchorCandidate(nsIFrame* aPrimaryFrame) const; + + // Search a frame's children to find an anchor node. Returns the frame for a + // valid anchor node, if one was found in the frames descendants, or null + // otherwise. + nsIFrame* FindAnchorIn(nsIFrame* aFrame) const; + + // Search a child list to find an anchor node. Returns the frame for a valid + // anchor node, if one was found in this child list, or null otherwise. + nsIFrame* FindAnchorInList(const nsFrameList& aFrameList) const; + // The owner of this scroll anchor container ScrollFrameHelper* mScrollFrame; + + // The anchor node that we will scroll to keep in the same relative position + // after reflows. This may be null if we were not able to select a valid + // scroll anchor + nsIFrame* mAnchorNode; + + // The last position of the scroll anchor node relative to the scrollable + // frame. This is used for calculating the distance to scroll to keep the + // anchor node in the same relative position + nsPoint mLastAnchorPos; + + // True if we should recalculate our anchor node at the next chance + bool mAnchorNodeIsDirty : 1; }; } // namespace layout diff --git a/layout/generic/nsFrame.cpp b/layout/generic/nsFrame.cpp index 2e5468e46feb..50d3fde61757 100644 --- a/layout/generic/nsFrame.cpp +++ b/layout/generic/nsFrame.cpp @@ -111,6 +111,7 @@ #include "mozilla/dom/TouchEvent.h" #include "mozilla/gfx/Tools.h" #include "mozilla/layers/WebRenderUserData.h" +#include "mozilla/layout/ScrollAnchorContainer.h" #include "nsPrintfCString.h" #include "ActiveLayerTracker.h" @@ -727,6 +728,11 @@ void nsFrame::DestroyFrom(nsIFrame* aDestructRoot, ActiveLayerTracker::TransferActivityToContent(this, mContent); } + ScrollAnchorContainer* anchor = nullptr; + if (IsScrollAnchor(&anchor)) { + anchor->InvalidateAnchor(); + } + if (HasCSSAnimations() || HasCSSTransitions() || EffectSet::GetEffectSet(this)) { // If no new frame for this element is created by the end of the @@ -9143,6 +9149,28 @@ void nsIFrame::ComputePreserve3DChildrenOverflow( } } +bool nsIFrame::IsScrollAnchor(ScrollAnchorContainer** aOutContainer) { + if (!mInScrollAnchorChain) { + return false; + } + + ScrollAnchorContainer* container = ScrollAnchorContainer::FindFor(this); + if (container->AnchorNode() != this) { + return false; + } + + if (aOutContainer) { + *aOutContainer = container; + } + return true; +} + +bool nsIFrame::IsInScrollAnchorChain() const { return mInScrollAnchorChain; } + +void nsIFrame::SetInScrollAnchorChain(bool aInChain) { + mInScrollAnchorChain = aInChain; +} + uint32_t nsIFrame::GetDepthInFrameTree() const { uint32_t result = 0; for (nsContainerFrame* ancestor = GetParent(); ancestor; diff --git a/layout/generic/nsGfxScrollFrame.cpp b/layout/generic/nsGfxScrollFrame.cpp index f7e922a9638f..128476fb66f3 100644 --- a/layout/generic/nsGfxScrollFrame.cpp +++ b/layout/generic/nsGfxScrollFrame.cpp @@ -2741,6 +2741,7 @@ void ScrollFrameHelper::ScrollToImpl(nsPoint aPt, const nsRect& aRange, } ScrollVisual(); + mAnchor.UserScrolled(); bool schedulePaint = true; if (nsLayoutUtils::AsyncPanZoomEnabled(mOuter) && @@ -4711,6 +4712,8 @@ void ScrollFrameHelper::AppendAnonymousContentTo( } void ScrollFrameHelper::Destroy(PostDestroyData& aPostDestroyData) { + mAnchor.Destroy(); + if (mScrollbarActivity) { mScrollbarActivity->Destroy(); mScrollbarActivity = nullptr; diff --git a/layout/generic/nsIFrame.h b/layout/generic/nsIFrame.h index 52d996b4f085..b262034d31bd 100644 --- a/layout/generic/nsIFrame.h +++ b/layout/generic/nsIFrame.h @@ -115,6 +115,10 @@ class Layer; class LayerManager; } // namespace layers +namespace layout { +class ScrollAnchorContainer; +} // namespace layout + namespace dom { class Selection; } // namespace dom @@ -566,7 +570,8 @@ class nsIFrame : public nsQueryFrame { mIsPrimaryFrame(false), mMayHaveTransformAnimation(false), mMayHaveOpacityAnimation(false), - mAllDescendantsAreInvisible(false) { + mAllDescendantsAreInvisible(false), + mInScrollAnchorChain(false) { mozilla::PodZero(&mOverflow); } @@ -1846,6 +1851,24 @@ class nsIFrame : public nsQueryFrame { void RecomputePerspectiveChildrenOverflow(const nsIFrame* aStartFrame); + /** + * Returns whether this frame is the anchor of some ancestor scroll frame. As + * this frame is moved, the scroll frame will apply adjustments to keep this + * scroll frame in the same relative position. + * + * aOutContainer will optionally be set to the scroll anchor container for + * this frame if this frame is an anchor. + */ + bool IsScrollAnchor( + mozilla::layout::ScrollAnchorContainer** aOutContainer = nullptr); + + /** + * Returns whether this frame is the anchor of some ancestor scroll frame, or + * has a descendant which is the scroll anchor. + */ + bool IsInScrollAnchorChain() const; + void SetInScrollAnchorChain(bool aInChain); + /** * Returns the number of ancestors between this and the root of our frame tree */ @@ -4293,9 +4316,12 @@ class nsIFrame : public nsQueryFrame { */ bool mAllDescendantsAreInvisible : 1; - protected: - // There is a 1-bit gap left here. + /** + * True if we are or contain the scroll anchor for a scrollable frame. + */ + bool mInScrollAnchorChain : 1; + protected: // Helpers /** * Can we stop inside this frame when we're skipping non-rendered whitespace?