commit f47abb0c30dff9ad971771ccfc5368f373111d80
parent 045b5b1cb3b67f4fc035b2958a18ffbf77966d31
Author: David Shin <dshin@mozilla.com>
Date: Thu, 30 Oct 2025 15:44:25 +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:
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;
}