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?