tor-browser

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

commit dd74b4671e2e98ac808e2cccbeb3edb09f816bf4
parent e09242cc50fc6de2a08cc172d2a3f43fbbd7914b
Author: David Shin <dshin@mozilla.com>
Date:   Fri, 31 Oct 2025 02:19:03 +0000

Bug 1968745: Keep track of default anchor and its nearest scrollers. r=layout-anchor-positioning-reviewers,layout-reviewers,jwatt,emilio

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

Diffstat:
Mlayout/base/AnchorPositioningUtils.cpp | 21+++++++++++++++++++++
Mlayout/base/AnchorPositioningUtils.h | 42++++++++++++++++++++++++++++++++++++++++++
Mlayout/generic/AbsoluteContainingBlock.cpp | 88++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
3 files changed, 141 insertions(+), 10 deletions(-)

diff --git a/layout/base/AnchorPositioningUtils.cpp b/layout/base/AnchorPositioningUtils.cpp @@ -6,6 +6,7 @@ #include "AnchorPositioningUtils.h" +#include "ScrollContainerFrame.h" #include "mozilla/Maybe.h" #include "mozilla/PresShell.h" #include "mozilla/dom/Document.h" @@ -384,6 +385,12 @@ const AnchorPosReferenceData::Value* AnchorPosReferenceData::Lookup( return mMap.Lookup(aAnchorName).DataPtrOrNull(); } +AnchorPosDefaultAnchorCache::AnchorPosDefaultAnchorCache( + const nsIFrame* aAnchor) + : mAnchor{aAnchor}, + mScrollContainer{AnchorPositioningUtils::GetNearestScrollFrame(aAnchor)} { +} + nsIFrame* AnchorPositioningUtils::FindFirstAcceptableAnchor( const nsAtom* aName, const nsIFrame* aPositionedFrame, const nsTArray<nsIFrame*>& aPossibleAnchorFrames) { @@ -581,6 +588,20 @@ nsRect AnchorPositioningUtils::AdjustAbsoluteContainingBlockRectForPositionArea( return res; } +const nsIFrame* AnchorPositioningUtils::GetNearestScrollFrame( + const nsIFrame* aFrame) { + if (!aFrame) { + return nullptr; + } + // `GetNearestScrollContainerFrame` will return the incoming frame if it's a + // scroll frame, so nudge to parent. + const nsIFrame* parent = aFrame->GetParent(); + return nsLayoutUtils::GetNearestScrollContainerFrame( + const_cast<nsIFrame*>(parent), + nsLayoutUtils::SCROLLABLE_SAME_DOC | + nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN); +} + // Out of line to avoid having to include AnchorPosReferenceData from nsIFrame.h void DeleteAnchorPosReferenceData(AnchorPosReferenceData* aData) { delete aData; diff --git a/layout/base/AnchorPositioningUtils.h b/layout/base/AnchorPositioningUtils.h @@ -54,6 +54,10 @@ class AnchorPosReferenceData { nsTHashMap<RefPtr<const nsAtom>, mozilla::Maybe<AnchorPosResolutionData>>; public: + struct Empty {}; + // Backup data for attempting a different `@position-try` style, when + // the default anchor remains the same. Empty, for now. + using PositionTryBackup = Empty; using Value = mozilla::Maybe<AnchorPosResolutionData>; AnchorPosReferenceData() = default; @@ -76,6 +80,12 @@ class AnchorPosReferenceData { Map::const_iterator begin() const { return mMap.cbegin(); } Map::const_iterator end() const { return mMap.cend(); } + PositionTryBackup TryPositionWithSameDefaultAnchor() { + return PositionTryBackup{}; + } + + void UndoTryPositionWithSameDefaultAnchor(PositionTryBackup&&) {} + private: Map mMap; }; @@ -88,6 +98,9 @@ struct AnchorPosDefaultAnchorCache { const nsIFrame* mAnchor = nullptr; // Scroll container for the default anchor. const nsIFrame* mScrollContainer = nullptr; + + AnchorPosDefaultAnchorCache() = default; + explicit AnchorPosDefaultAnchorCache(const nsIFrame* aAnchor); }; // Cache data used by anchor resolution. To be populated on abspos reflow, @@ -99,6 +112,33 @@ struct AnchorPosResolutionCache { // Cached data for default anchor resolution. Designed to be short-lived, // so it can contain e.g. frame pointers. AnchorPosDefaultAnchorCache mDefaultAnchorCache; + + // Backup data for attempting a different `@position-try` style, when + // the default anchor remains the same. + using PositionTryBackup = AnchorPosReferenceData::PositionTryBackup; + PositionTryBackup TryPositionWithSameDefaultAnchor() { + return mReferenceData->TryPositionWithSameDefaultAnchor(); + } + void UndoTryPositionWithSameDefaultAnchor(PositionTryBackup&& aBackup) { + mReferenceData->UndoTryPositionWithSameDefaultAnchor(std::move(aBackup)); + } + + // Backup data for attempting a different `@position-try` style, when + // the default anchor changes. + using PositionTryFullBackup = + std::pair<AnchorPosReferenceData, AnchorPosDefaultAnchorCache>; + PositionTryFullBackup TryPositionWithDifferentDefaultAnchor() { + auto referenceData = std::move(*mReferenceData); + *mReferenceData = {}; + return std::make_pair( + std::move(referenceData), + std::exchange(mDefaultAnchorCache, AnchorPosDefaultAnchorCache{})); + } + void UndoTryPositionWithDifferentDefaultAnchor( + PositionTryFullBackup&& aBackup) { + *mReferenceData = std::move(aBackup.first); + std::exchange(mDefaultAnchorCache, aBackup.second); + } }; enum class StylePositionTryFallbacksTryTacticKeyword : uint8_t; @@ -179,6 +219,8 @@ struct AnchorPositioningUtils { static DefaultAnchorInfo GetDefaultAnchor( const nsIFrame* aPositioned, bool aCBRectIsValid, AnchorPosReferenceData* aAnchorPosReferenceData); + + static const nsIFrame* GetNearestScrollFrame(const nsIFrame* aFrame); }; } // namespace mozilla diff --git a/layout/generic/AbsoluteContainingBlock.cpp b/layout/generic/AbsoluteContainingBlock.cpp @@ -155,6 +155,34 @@ static bool IsSnapshotContainingBlock(const nsIFrame* aFrame) { PseudoStyleType::mozSnapshotContainingBlock; } +static AnchorPosResolutionCache PopulateAnchorResolutionCache( + const nsIFrame* aKidFrame, AnchorPosReferenceData* aData) { + MOZ_ASSERT(aKidFrame->HasAnchorPosReference()); + // If the default anchor exists, it will likely be referenced (Except when + // authors then use `anchor()` without referring to anchors whose nearest + // scroller that of the default anchor, but that seems + // counter-productive). This is a prerequisite for scroll compensation. We + // also need to check for `anchor()` resolutions, so cache information for + // default anchor and its scrollers right now. + AnchorPosDefaultAnchorCache defaultAnchorCache; + const auto* defaultAnchorName = + AnchorPositioningUtils::GetUsedAnchorName(aKidFrame, nullptr); + if (defaultAnchorName) { + const auto* anchor = aKidFrame->PresShell()->GetAnchorPosAnchor( + defaultAnchorName, aKidFrame); + defaultAnchorCache = AnchorPosDefaultAnchorCache{anchor}; + if (anchor) { + const auto entryData = aData->InsertOrModify(defaultAnchorName, false); + MOZ_ASSERT(!entryData.mAlreadyResolved); + // Put it in the cache with size resolved. + // TODO(dshin): May as well resolve offsets here? + *entryData.mEntry = + Some(AnchorPosResolutionData{anchor->GetSize(), Nothing{}}); + } + } + return {aData, defaultAnchorCache}; +} + void AbsoluteContainingBlock::Reflow(nsContainerFrame* aDelegatingFrame, nsPresContext* aPresContext, const ReflowInput& aReflowInput, @@ -179,10 +207,10 @@ void AbsoluteContainingBlock::Reflow(nsContainerFrame* aDelegatingFrame, for (nsIFrame* kidFrame : mAbsoluteFrames) { Maybe<AnchorPosResolutionCache> anchorPosResolutionCache; if (kidFrame->HasAnchorPosReference()) { - anchorPosResolutionCache = Some(AnchorPosResolutionCache{}); - anchorPosResolutionCache->mReferenceData = - kidFrame->SetOrUpdateDeletableProperty( - nsIFrame::AnchorPosReferences()); + auto* referenceData = kidFrame->SetOrUpdateDeletableProperty( + nsIFrame::AnchorPosReferences()); + anchorPosResolutionCache = + Some(PopulateAnchorResolutionCache(kidFrame, referenceData)); } else { kidFrame->RemoveProperty(nsIFrame::AnchorPosReferences()); } @@ -844,23 +872,56 @@ void AbsoluteContainingBlock::ResolveAutoMarginsAfterLayout( } } +struct None {}; +using OldCacheState = Variant<None, AnchorPosResolutionCache::PositionTryBackup, + AnchorPosResolutionCache::PositionTryFullBackup>; + struct MOZ_STACK_CLASS MOZ_RAII AutoFallbackStyleSetter { - AutoFallbackStyleSetter(nsIFrame* aFrame, ComputedStyle* aFallbackStyle) - : mFrame(aFrame) { + AutoFallbackStyleSetter(nsIFrame* aFrame, ComputedStyle* aFallbackStyle, + AnchorPosResolutionCache* aCache, bool aIsFirstTry) + : mFrame(aFrame), mCache{aCache}, mOldCacheState{None{}} { if (aFallbackStyle) { mOldStyle = aFrame->SetComputedStyleWithoutNotification(aFallbackStyle); } + // We need to be able to "go back" to the old, first try (Which is not + // necessarily base style) cache. + if (!aIsFirstTry && aCache) { + // New fallback could just be a flip keyword. + if (mOldStyle && mOldStyle->StylePosition()->mPositionAnchor != + aFrame->StylePosition()->mPositionAnchor) { + mOldCacheState = + OldCacheState{aCache->TryPositionWithDifferentDefaultAnchor()}; + *aCache = PopulateAnchorResolutionCache(aFrame, aCache->mReferenceData); + } else { + mOldCacheState = + OldCacheState{aCache->TryPositionWithSameDefaultAnchor()}; + } + } } ~AutoFallbackStyleSetter() { if (mOldStyle) { mFrame->SetComputedStyleWithoutNotification(std::move(mOldStyle)); } + std::move(mOldCacheState) + .match( + [](None&&) {}, + [&](AnchorPosResolutionCache::PositionTryBackup&& aBackup) { + mCache->UndoTryPositionWithSameDefaultAnchor(std::move(aBackup)); + }, + [&](AnchorPosResolutionCache::PositionTryFullBackup&& aBackup) { + mCache->UndoTryPositionWithDifferentDefaultAnchor( + std::move(aBackup)); + }); } + void CommitCurrentFallback() { mOldCacheState = OldCacheState{None{}}; } + private: nsIFrame* const mFrame; RefPtr<ComputedStyle> mOldStyle; + AnchorPosResolutionCache* const mCache; + OldCacheState mOldCacheState; }; // XXX Optimize the case where it's a resize reflow and the absolutely @@ -940,14 +1001,19 @@ void AbsoluteContainingBlock::ReflowAbsoluteFrame( return SeekFallbackTo(nextFallbackIndex); }; + Maybe<uint32_t> firstTryIndex; // TODO(emilio): Right now fallback only applies to position-area, which only // makes a difference with a default anchor... Generalize it? if (aAnchorPosResolutionCache) { bool found = false; uint32_t index = aKidFrame->GetProperty( nsIFrame::LastSuccessfulPositionFallback(), &found); - if (found && !SeekFallbackTo(index)) { - aKidFrame->RemoveProperty(nsIFrame::LastSuccessfulPositionFallback()); + if (found) { + if (!SeekFallbackTo(index)) { + aKidFrame->RemoveProperty(nsIFrame::LastSuccessfulPositionFallback()); + } else { + firstTryIndex = Some(index); + } } } @@ -956,7 +1022,9 @@ void AbsoluteContainingBlock::ReflowAbsoluteFrame( bool isOverflowingCB = true; do { - AutoFallbackStyleSetter fallback(aKidFrame, currentFallbackStyle); + AutoFallbackStyleSetter fallback(aKidFrame, currentFallbackStyle, + aAnchorPosResolutionCache, + firstTryIndex == currentFallbackIndex); auto positionArea = aKidFrame->StylePosition()->mPositionArea; StylePositionArea resolvedPositionArea; const nsRect usedCb = [&] { @@ -1209,8 +1277,8 @@ void AbsoluteContainingBlock::ReflowAbsoluteFrame( if (fallbacks.IsEmpty() || fits) { // We completed the reflow - Either we had a fallback that fit, or we // didn't have any to try in the first place. - // TODO(dshin, bug 1987963): Hypothetical scroll will be committed here. isOverflowingCB = !fits; + fallback.CommitCurrentFallback(); break; }