tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 7b0c7c8034392fdb8cd13f220973f47927c4bfc4
parent 453decad1a322d0f4fed450cd44fb2cd68c4a1b1
Author: David Shin <dshin@mozilla.com>
Date:   Wed, 17 Dec 2025 19:11:35 +0000

Bug 2006002: Ensure that positioned elements with implicit anchors get scroll compensated. r=layout-anchor-positioning-reviewers,firefox-style-system-reviewers,layout-reviewers,emilio

Differential Revision: https://phabricator.services.mozilla.com/D276884

Diffstat:
Mlayout/base/PresShell.cpp | 66++++++++++++++++++++++++++++++++++++++++++------------------------
Mlayout/base/PresShell.h | 3+++
Mlayout/style/ComputedStyle.cpp | 9++++-----
Atesting/web-platform/tests/css/css-anchor-position/anchor-scroll-implicit-001.html | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Atesting/web-platform/tests/css/css-anchor-position/anchor-scroll-implicit-002.html | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 162 insertions(+), 29 deletions(-)

diff --git a/layout/base/PresShell.cpp b/layout/base/PresShell.cpp @@ -11571,17 +11571,26 @@ struct AffectedAnchorGroup { nsTArray<AffectedAnchor> mFrames; }; +static const nsIFrame* NearestScrollContainerOfAffectedAnchor( + const nsIFrame* aAnchor, const ScrollContainerFrame* aScrollContainer) { + const auto* scrollContainer = + AnchorPositioningUtils::GetNearestScrollFrame(aAnchor).mScrollContainer; + if (!scrollContainer) { + // Fixed-pos anchor, likely + return nullptr; + } + // Does this scroll container match a anchor's nearest scroll container, + // or contain it? + if (scrollContainer == aScrollContainer || + nsLayoutUtils::IsProperAncestorFrame(aScrollContainer, scrollContainer)) { + return scrollContainer; + } + return nullptr; +} + static nsTArray<AffectedAnchorGroup> FindAnchorsAffectedByScroll( const nsTHashMap<RefPtr<const nsAtom>, nsTArray<nsIFrame*>>& aAnchors, const ScrollContainerFrame* aScrollContainer) { - const auto AffectedByScrollContainer = - [](const nsIFrame* aFrame, const ScrollContainerFrame* aScrollContainer) { - MOZ_ASSERT(aFrame); - MOZ_ASSERT(aScrollContainer); - return aFrame == aScrollContainer || - nsLayoutUtils::IsProperAncestorFrame(aScrollContainer, aFrame); - }; - nsTArray<AffectedAnchorGroup> affectedAnchors; // We keep only referenced anchors' name in positioned frames to avoid dealing // with lifetime issues associated with it. Now we need to re-establish that @@ -11591,14 +11600,8 @@ static nsTArray<AffectedAnchorGroup> FindAnchorsAffectedByScroll( Maybe<nsTArray<AffectedAnchor>> affected; for (const auto& frame : anchorFrames) { const auto* scrollContainer = - AnchorPositioningUtils::GetNearestScrollFrame(frame).mScrollContainer; + NearestScrollContainerOfAffectedAnchor(frame, aScrollContainer); if (!scrollContainer) { - // Fixed-pos anchor, likely - continue; - } - // Does this scroll container match a anchor's nearest scroll container, - // or contain it? - if (!AffectedByScrollContainer(scrollContainer, aScrollContainer)) { continue; } if (affected.isNothing()) { @@ -11616,8 +11619,9 @@ static nsTArray<AffectedAnchorGroup> FindAnchorsAffectedByScroll( // Given a list of anchors affected by scrolling, find one that the given // positioned frame need to compensate scroll for. -static Maybe<const AffectedAnchor&> FindScrollCompensatedAnchor( +static Maybe<AffectedAnchor> FindScrollCompensatedAnchor( const PresShell* aPresShell, + const ScrollContainerFrame* aScrolledScrollContainer, const nsTArray<AffectedAnchorGroup>& aAffectedAnchors, const nsIFrame* aPositioned, const AnchorPosReferenceData& aReferenceData, const nsIFrame** aResolvedDefaultAnchor) { @@ -11650,6 +11654,22 @@ static Maybe<const AffectedAnchor&> FindScrollCompensatedAnchor( return Nothing{}; } + if (defaultAnchorName == nsGkAtoms::AnchorPosImplicitAnchor) { + // We're not going to find this in `aAffectedAnchors`, which works off of + // `PresShell::mAnchorPosAnchors`, which doesn't store implicit anchors. + const auto* anchor = + AnchorPositioningUtils::GetAnchorPosImplicitAnchor(aPositioned); + if (!anchor) { + return Nothing{}; + } + const auto* scrollContainer = NearestScrollContainerOfAffectedAnchor( + anchor, aScrolledScrollContainer); + if (!scrollContainer) { + return Nothing{}; + } + return Some(AffectedAnchor{anchor, scrollContainer}); + } + struct Comparator { bool Equals(const AffectedAnchor& aEntry, const nsIFrame* aFrame) const { return aEntry.mAnchor == aFrame; @@ -11672,7 +11692,7 @@ static Maybe<const AffectedAnchor&> FindScrollCompensatedAnchor( break; } const auto& info = anchors.ElementAt(idx); - return SomeRef(info); + return Some(info); } return Nothing{}; @@ -11726,7 +11746,7 @@ static bool AnchorIsStickyOrChainedToScrollCompensatedAnchor( // https://drafts.csswg.org/css-anchor-position-1/#default-scroll-shift void PresShell::UpdateAnchorPosForScroll( const ScrollContainerFrame* aScrollContainer) { - if (mAnchorPosAnchors.IsEmpty()) { + if (mAnchorPosAnchors.IsEmpty() && mAnchorPosPositioned.IsEmpty()) { return; } @@ -11737,10 +11757,7 @@ void PresShell::UpdateAnchorPosForScroll( // can. nsTArray<AffectedAnchorGroup> affectedAnchors = FindAnchorsAffectedByScroll(mAnchorPosAnchors, aScrollContainer); - - if (affectedAnchors.IsEmpty()) { - return; - } + // Affected anchors may be empty, an implicit anchor may have scrolled. // Now, update all affected positioned elements' scroll offsets. for (auto* positioned : mAnchorPosPositioned) { @@ -11750,8 +11767,9 @@ void PresShell::UpdateAnchorPosForScroll( continue; } const nsIFrame* defaultAnchor = nullptr; - const auto scrollDependency = FindScrollCompensatedAnchor( - this, affectedAnchors, positioned, *referenceData, &defaultAnchor); + const auto scrollDependency = + FindScrollCompensatedAnchor(this, aScrollContainer, affectedAnchors, + positioned, *referenceData, &defaultAnchor); const bool offsetChanged = [&]() { if (!scrollDependency) { return false; diff --git a/layout/base/PresShell.h b/layout/base/PresShell.h @@ -3261,6 +3261,9 @@ class PresShell final : public nsStubDocumentObserver, // cannot be determined. nsTArray<AnchorPosAnchorChange> mLazyAnchorPosAnchorChanges; + // Note: Does not store implicit anchors, since many elements can be + // potential implicit anchors (e.g. pseudo-elements' implicit anchor + // is its originating element). nsTHashMap<RefPtr<const nsAtom>, nsTArray<nsIFrame*>> mAnchorPosAnchors; nsTArray<nsIFrame*> mAnchorPosPositioned; diff --git a/layout/style/ComputedStyle.cpp b/layout/style/ComputedStyle.cpp @@ -432,11 +432,10 @@ void ComputedStyle::DumpMatchedRules() const { bool ComputedStyle::HasAnchorPosReference() const { const auto* pos = StylePosition(); - if (pos->mPositionAnchor.IsIdent()) { - // Short circuit if there's a default anchor defined, even if - // it may not end up being referenced. - // If this early return is removed, we'll need to handle mPositionArea - // explicitly. + if (pos->mPositionAnchor.IsIdent() || pos->mPositionAnchor.IsAuto()) { + // Short circuit if there's a default anchor defined (Or an implicit one), + // even if it may not end up being referenced. If this early return is + // removed, we'll need to handle mPositionArea explicitly. return true; } diff --git a/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-implicit-001.html b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-implicit-001.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<html class=reftest-wait> +<title>Tests that scroll adjustments of implicitly anchored elements are applied correctly</title> +<link rel="author" href="mailto:dshin@mozilla.com"> +<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/"> +<link rel="match" href="../reference/ref-filled-green-100px-square.xht"> +<style> +.positioned { + position: fixed; + width: 100px; + height: 50px; + left: anchor(left); + top: anchor(bottom); + background: green; + border: none; + padding: 0; + margin: 0; +} + +.container { + position: relative; + width: 100px; + height: 100px; + background: red; + overflow: hidden; +} + +.filler { + width: 1px; + height: 50px; +} + +.anchor { + width: 100px; + height: 50px; + background: green; + border: none; + padding: 0; +} +</style> +<p>Test passes if there is a filled green square and <strong>no red</strong>.</p> +<div class=positioned popover id="popover"></div> +<div id=s class=container> + <div class=filler></div> + <button id=b class=anchor popovertarget="popover"></button> + <div class=filler></div> +</div> +<script> +b.click(); +s.scrollTop = 50; +document.documentElement.classList.remove('reftest-wait'); +</script> +</html> diff --git a/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-implicit-002.html b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-implicit-002.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<html class=reftest-wait> +<title>Tests that scroll adjustments of implicitly anchored elements are applied correctly</title> +<link rel="author" href="mailto:dshin@mozilla.com"> +<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/"> +<link rel="match" href="../reference/ref-filled-green-100px-square.xht"> +<style> +.positioned { + position: absolute; + width: 100px; + height: 50px; + left: anchor(left); + top: anchor(bottom); + background: green; + border: none; + padding: 0; + margin: 0; +} + +.container { + position: relative; + width: 100px; + height: 100px; + background: red; +} + +.scroller { + width: 100%; + height: 100%; + overflow: hidden; +} + +.filler { + width: 1px; + height: 50px; +} + +.anchor { + width: 100px; + height: 50px; + background: green; + border: none; + padding: 0; +} +</style> +<p>Test passes if there is a filled green square and <strong>no red</strong>.</p> +<div class=container> + <div id=s class=scroller> + <div class=filler></div> + <button id=b class=anchor popovertarget="popover"></button> + <div class=filler></div> + </div> + <div class=positioned popover id="popover"></div> +</div> +<script> +b.click(); +s.scrollTop = 50; +document.documentElement.classList.remove('reftest-wait'); +</script> +</html>