commit f20c84dcfe39cad703a33b4617f180d9c96db3b4
parent 5ba984e00abbd0ad7aa40abb624865c586907a9a
Author: David Shin <dshin@mozilla.com>
Date: Fri, 31 Oct 2025 02:19:03 +0000
Bug 1987963: Apply default scroll offset on scroll. r=layout-reviewers,tnikkel,jwatt
Differential Revision: https://phabricator.services.mozilla.com/D266469
Diffstat:
7 files changed, 382 insertions(+), 171 deletions(-)
diff --git a/layout/base/AnchorPositioningUtils.cpp b/layout/base/AnchorPositioningUtils.cpp
@@ -391,6 +391,17 @@ AnchorPosDefaultAnchorCache::AnchorPosDefaultAnchorCache(
mScrollContainer{AnchorPositioningUtils::GetNearestScrollFrame(aAnchor)} {
}
+AnchorPosDefaultAnchorCache::AnchorPosDefaultAnchorCache(
+ const nsIFrame* aAnchor, const nsIFrame* aScrollContainer)
+ : mAnchor{aAnchor}, mScrollContainer{aScrollContainer} {
+ MOZ_ASSERT_IF(
+ aAnchor,
+ nsLayoutUtils::GetNearestScrollContainerFrame(
+ const_cast<nsContainerFrame*>(aAnchor->GetParent()),
+ nsLayoutUtils::SCROLLABLE_SAME_DOC |
+ nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN) == mScrollContainer);
+}
+
nsIFrame* AnchorPositioningUtils::FindFirstAcceptableAnchor(
const nsAtom* aName, const nsIFrame* aPositionedFrame,
const nsTArray<nsIFrame*>& aPossibleAnchorFrames) {
@@ -701,6 +712,42 @@ const nsIFrame* AnchorPositioningUtils::GetNearestScrollFrame(
nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
}
+nsPoint AnchorPositioningUtils::GetScrollOffsetFor(
+ PhysicalAxes aAxes, const nsIFrame* aPositioned,
+ const AnchorPosDefaultAnchorCache& aDefaultAnchorCache) {
+ MOZ_ASSERT(aPositioned);
+ if (!aDefaultAnchorCache.mAnchor || aAxes.isEmpty()) {
+ return nsPoint{};
+ }
+ nsPoint offset;
+ const bool trackHorizontal = aAxes.contains(PhysicalAxis::Horizontal);
+ const bool trackVertical = aAxes.contains(PhysicalAxis::Vertical);
+ // TODO(dshin, bug 1991489): Traverse properly, in case anchor and positioned
+ // elements are in different continuation frames of the absolute containing
+ // block.
+ const auto* absoluteContainingBlock = aPositioned->GetParent();
+ if (GetNearestScrollFrame(aPositioned) ==
+ aDefaultAnchorCache.mScrollContainer) {
+ // Would scroll together anyway, skip.
+ return nsPoint{};
+ }
+ // Grab the accumulated offset up to, but not including, the abspos
+ // container.
+ for (const auto* f = aDefaultAnchorCache.mScrollContainer;
+ f && f != absoluteContainingBlock; f = f->GetParent()) {
+ if (const ScrollContainerFrame* scrollFrame = do_QueryFrame(f)) {
+ const auto o = scrollFrame->GetScrollPosition();
+ if (trackHorizontal) {
+ offset.x += o.x;
+ }
+ if (trackVertical) {
+ offset.y += o.y;
+ }
+ }
+ }
+ return offset;
+}
+
// Out of line to avoid having to include AnchorPosReferenceData from nsIFrame.h
void DeleteAnchorPosReferenceData(AnchorPosReferenceData* aData) {
delete aData;
@@ -766,4 +813,12 @@ const nsIFrame* AnchorPositioningUtils::GetAnchorPosImplicitAnchor(
: pseudoRootFrame->GetParent();
}
+bool AnchorPositioningUtils::FitsInContainingBlock(
+ const nsRect& aOverflowCheckRect,
+ const nsRect& aOriginalContainingBlockSize, const nsRect& aRect) {
+ return aOverflowCheckRect.Intersect(aOriginalContainingBlockSize)
+ .Union(aOriginalContainingBlockSize)
+ .Contains(aRect);
+}
+
} // namespace mozilla
diff --git a/layout/base/AnchorPositioningUtils.h b/layout/base/AnchorPositioningUtils.h
@@ -54,7 +54,7 @@ struct AnchorPosResolutionData {
// * If valid anchors are found,
// * Cached offset/size resolution, if resolution was valid,
// * Compensating for scroll [1]
-// * TODO(dshin, bug 1987962): Default scroll shift [2]
+// * Default scroll shift [2]
//
// [1]: https://drafts.csswg.org/css-anchor-position-1/#compensate-for-scroll
// [2]: https://drafts.csswg.org/css-anchor-position-1/#default-scroll-shift
@@ -66,7 +66,7 @@ class AnchorPosReferenceData {
public:
// Backup data for attempting a different `@position-try` style, when
// the default anchor remains the same.
- using PositionTryBackup = mozilla::PhysicalAxes;
+ using PositionTryBackup = std::pair<mozilla::PhysicalAxes, nsPoint>;
using Value = mozilla::Maybe<AnchorPosResolutionData>;
AnchorPosReferenceData() = default;
@@ -98,15 +98,21 @@ class AnchorPosReferenceData {
}
PositionTryBackup TryPositionWithSameDefaultAnchor() {
- const auto compensatingForScroll =
+ mozilla::PhysicalAxes compensatingForScroll =
std::exchange(mCompensatingForScroll, {});
- return compensatingForScroll;
+ nsPoint defaultScrollShift = std::exchange(mDefaultScrollShift, {});
+ return std::make_pair(compensatingForScroll, defaultScrollShift);
}
void UndoTryPositionWithSameDefaultAnchor(PositionTryBackup&& aBackup) {
- mCompensatingForScroll = aBackup;
+ std::tie(mCompensatingForScroll, mDefaultScrollShift) = aBackup;
}
+ // https://drafts.csswg.org/css-anchor-position-1/#default-scroll-shift
+ nsPoint mDefaultScrollShift;
+ // TODO(dshin, bug 1987962): Remembered scroll offset
+ // https://drafts.csswg.org/css-anchor-position-1/#remembered-scroll-offset
+
private:
ResolutionMap mMap;
// Axes we need to compensate for scroll [1] in.
@@ -125,6 +131,8 @@ struct AnchorPosDefaultAnchorCache {
AnchorPosDefaultAnchorCache() = default;
explicit AnchorPosDefaultAnchorCache(const nsIFrame* aAnchor);
+ AnchorPosDefaultAnchorCache(const nsIFrame* aAnchor,
+ const nsIFrame* aScrollContainer);
};
// Cache data used by anchor resolution. To be populated on abspos reflow,
@@ -233,6 +241,14 @@ struct AnchorPositioningUtils {
static const nsIFrame* GetAnchorPosImplicitAnchor(const nsIFrame* aFrame);
static const nsIFrame* GetNearestScrollFrame(const nsIFrame* aFrame);
+
+ static nsPoint GetScrollOffsetFor(
+ PhysicalAxes aAxes, const nsIFrame* aPositioned,
+ const AnchorPosDefaultAnchorCache& aDefaultAnchorCache);
+
+ static bool FitsInContainingBlock(const nsRect& aOverflowCheckRect,
+ const nsRect& aOriginalContainingBlockSize,
+ const nsRect& aRect);
};
} // namespace mozilla
diff --git a/layout/base/PresShell.cpp b/layout/base/PresShell.cpp
@@ -11737,179 +11737,164 @@ PresShell::AnchorPosUpdateResult PresShell::UpdateAnchorPosLayout() {
return result;
}
-static ScrollContainerFrame* FindScrollContainerFrameOf(
- const nsIFrame* aFrame) {
- MOZ_ASSERT(aFrame, "NULL frame for FindScrollContainerFrameOf()");
- auto* parent = aFrame->GetParent();
- return nsLayoutUtils::GetNearestScrollContainerFrame(
- parent, nsLayoutUtils::SCROLLABLE_SAME_DOC |
- nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
-}
-
-static bool UnderScrollContainer(nsIFrame* aFrame,
- const ScrollContainerFrame* aScrollContainer) {
- MOZ_ASSERT(aFrame);
- MOZ_ASSERT(aScrollContainer);
- return aFrame == aScrollContainer ||
- nsLayoutUtils::IsProperAncestorFrame(aScrollContainer, aFrame);
-}
-
-void PresShell::UpdateAnchorPosLayoutForScroll(
- ScrollContainerFrame* aScrollContainer) {
- if (mAnchorPosAnchors.IsEmpty()) {
- return;
- }
+using AffectedAnchor = AnchorPosDefaultAnchorCache;
+struct AffectedAnchorGroup {
+ const nsAtom* mAnchorName;
+ nsTArray<AffectedAnchor> mFrames;
+};
- AUTO_PROFILER_MARKER_UNTYPED("UpdateAnchorPosLayoutForScroll", LAYOUT, {});
- // TODO(dshin, bug 1923401): What follows is a non-spec compliant
- // implementation of scroll handling for anchor positioning. Specifically, it
- // will compensate for any offset between anchor and all positioned elements,
- // instead of compensating in axes anchoring to elements in the same scroller
- // as the default anchor. As a result, positioned elements will resize to
- // stay attached to all anchors. This needs to be adressed after we stop
- // storing anchor offset that includes scroll offsets.
- // TODO(dshin, bug 1987463): After bug 1923401, we still need this code path
- // to update the scroll offset, so it's worth investigating further
- // optimizations.
- struct AffectedAnchor {
- const nsIFrame* mFrame;
- ScrollContainerFrame* mNearestScrollContainer;
- };
- struct AffectedAnchorGroup {
- const nsAtom* mAnchorName;
- nsTArray<AffectedAnchor> mFrames;
- };
- struct Comparator {
- bool Equals(const AffectedAnchor& aEntry, const nsIFrame* aFrame) const {
- return aEntry.mFrame == aFrame;
- }
- };
+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);
+ };
- // First, find all anchors under this scroll container. Can look at positioned
- // frames' anchor references first, but we want to avoid anchor lookups if we
- // can.
nsTArray<AffectedAnchorGroup> affectedAnchors;
- for (const auto& kv : mAnchorPosAnchors) {
+ // 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
+ // association.
+ for (const auto& kv : aAnchors) {
const auto& anchorFrames = kv.GetData();
Maybe<nsTArray<AffectedAnchor>> affected;
for (const auto& frame : anchorFrames) {
- auto* nearestScrollFrame = FindScrollContainerFrameOf(frame);
- if (!nearestScrollFrame) {
- // Fixed-pos anchor.
+ const auto* scrollContainer =
+ AnchorPositioningUtils::GetNearestScrollFrame(frame);
+ if (!scrollContainer) {
+ // Fixed-pos anchor, likely
continue;
}
- if (!UnderScrollContainer(nearestScrollFrame, aScrollContainer)) {
+ // Does this scroll container match a anchor's nearest scroll container,
+ // or contain it?
+ if (!AffectedByScrollContainer(scrollContainer, aScrollContainer)) {
continue;
}
if (affected.isNothing()) {
affected = Some(nsTArray<AffectedAnchor>{anchorFrames.Length()});
}
- affected.ref().AppendElement(AffectedAnchor{frame, nearestScrollFrame});
+ affected.ref().AppendElement(AffectedAnchor{frame, scrollContainer});
}
if (affected.isSome()) {
affectedAnchors.AppendElement(
AffectedAnchorGroup{kv.GetKey(), std::move(*affected)});
}
}
+ return affectedAnchors;
+}
- if (affectedAnchors.IsEmpty()) {
- return;
+struct FindScrollCompensatedAnchorResult {
+ const AffectedAnchor& mAnchorInfo;
+ AnchorPosReferenceData& mReferenceData;
+};
+
+// Given a list of anchors affected by scrolling, find one that the given
+// positioned frame need to compensate scroll for.
+static Maybe<FindScrollCompensatedAnchorResult> FindScrollCompensatedAnchor(
+ const PresShell* aPresShell,
+ const nsTArray<AffectedAnchorGroup>& aAffectedAnchors,
+ const nsIFrame* aPositioned) {
+ MOZ_ASSERT(aPositioned->IsAbsolutelyPositioned(),
+ "Anchor positioned frame is not absolutely positioned?");
+ const auto* defaultAnchorName =
+ AnchorPositioningUtils::GetUsedAnchorName(aPositioned, nullptr);
+ if (!defaultAnchorName) {
+ return Nothing{};
}
- // Now, find positioned frames that depend on anchors under the scroll frame.
- for (auto* positioned : mAnchorPosPositioned) {
- MOZ_ASSERT(positioned->IsAbsolutelyPositioned(),
- "Anchor positioned frame is not absolutely positioned?");
- if (positioned->HasAnyStateBits(NS_FRAME_IS_DIRTY)) {
- // Already dirty? Skip.
- continue;
- }
+ auto* referencedAnchors =
+ aPositioned->GetProperty(nsIFrame::AnchorPosReferences());
+ if (!referencedAnchors || referencedAnchors->IsEmpty()) {
+ return Nothing{};
+ }
- const auto* anchorPosReferenceData =
- positioned->GetProperty(nsIFrame::AnchorPosReferences());
- if (!anchorPosReferenceData || anchorPosReferenceData->IsEmpty()) {
- // If it doesn't reference any anchors then it doesn't compensate for
- // scroll.
- continue;
+ const auto compensatingForScroll =
+ referencedAnchors->CompensatingForScrollAxes();
+ if (compensatingForScroll.isEmpty()) {
+ return Nothing{};
+ }
+
+ struct Comparator {
+ bool Equals(const AffectedAnchor& aEntry, const nsIFrame* aFrame) const {
+ return aEntry.mAnchor == aFrame;
}
+ };
- const nsAtom* defaultAnchorName =
- AnchorPositioningUtils::GetUsedAnchorName(positioned, nullptr);
- if (!defaultAnchorName) {
+ // Find the relevant default anchor.
+ nsIFrame const* defaultAnchor = nullptr;
+ for (const auto& group : aAffectedAnchors) {
+ if (defaultAnchorName &&
+ !group.mAnchorName->Equals(defaultAnchorName->GetUTF16String(),
+ defaultAnchorName->GetLength())) {
+ // Default anchor has a name, and it's different from this affected
+ // anchor group.
continue;
}
- // We might not need to do this GetAnchorPosAnchor call at all if none of
- // the affected anchors are referenced by positioned below. We could
- // improve this.
- const nsIFrame* defaultAnchorFrame =
- GetAnchorPosAnchor(defaultAnchorName, positioned);
- if (!defaultAnchorFrame) {
- continue;
+ if (!defaultAnchor) {
+ defaultAnchor =
+ aPresShell->GetAnchorPosAnchor(defaultAnchorName, aPositioned);
+ if (!defaultAnchor) {
+ // Default anchor not valid for this positioned frame.
+ return Nothing{};
+ }
}
- auto* nearestScrollToDefaultAnchor =
- FindScrollContainerFrameOf(defaultAnchorFrame);
+ const auto& anchors = group.mFrames;
+ // Find the affected anchor that not only matches in name, but in actual
+ // frame.
+ const auto idx = anchors.IndexOf(defaultAnchor, 0, Comparator{});
+ if (idx == anchors.NoIndex) {
+ // We found the default anchor, but it wasn't correct.
+ break;
+ }
+ const auto& info = anchors.ElementAt(idx);
+ return Some(FindScrollCompensatedAnchorResult{info, *referencedAnchors});
+ }
- auto* absoluteContainingBlock = positioned->GetParent();
+ return Nothing{};
+}
- if (UnderScrollContainer(absoluteContainingBlock,
- nearestScrollToDefaultAnchor)) {
- // If the positioned element's containing block is under the only possible
- // anchor scroll container that it can scroll with, they'll scroll
- // together without intervention, so skip the update.
- continue;
- }
+// https://drafts.csswg.org/css-anchor-position-1/#default-scroll-shift
+void PresShell::UpdateAnchorPosForScroll(
+ const ScrollContainerFrame* aScrollContainer) const {
+ if (mAnchorPosAnchors.IsEmpty()) {
+ return;
+ }
- const auto* stylePos = positioned->StylePosition();
- if (!stylePos->mPositionAnchor.IsIdent()) {
- auto* nearestScrollFrame = FindScrollContainerFrameOf(defaultAnchorFrame);
- if (!nearestScrollFrame) {
- // Fixed-pos anchor.
- continue;
- }
- if (!UnderScrollContainer(nearestScrollFrame, aScrollContainer)) {
- continue;
- }
- const auto* data = anchorPosReferenceData->Lookup(defaultAnchorName);
- if (!data) {
- continue;
- }
- if (NeedReflowForAnchorPos(defaultAnchorFrame, positioned, *data)) {
- // Abspos frames should not affect ancestor intrinsics.
- FrameNeedsReflow(positioned, IntrinsicDirty::None,
- NS_FRAME_HAS_DIRTY_CHILDREN);
- }
- continue;
- }
+ AUTO_PROFILER_MARKER_UNTYPED("UpdateAnchorPosForScroll", LAYOUT, {});
- for (const auto& entry : affectedAnchors) {
- const auto* anchorName = entry.mAnchorName;
- const auto& anchors = entry.mFrames;
- const auto* data = anchorPosReferenceData->Lookup(anchorName);
- if (!data) {
- continue;
- }
- const auto* anchorFrame =
- anchorName == defaultAnchorName
- ? defaultAnchorFrame
- : GetAnchorPosAnchor(anchorName, positioned);
- const auto idx = anchors.IndexOf(anchorFrame, 0, Comparator{});
- if (idx == nsTArray<AffectedAnchor>::NoIndex) {
- // Referring to an anchor of the same name but unaffected by scrolling -
- // skip.
- continue;
- }
- auto* anchorScrollContainer =
- anchors.ElementAt(idx).mNearestScrollContainer;
- if (anchorScrollContainer != nearestScrollToDefaultAnchor) {
- // We do not compensate for scroll for this anchor
- continue;
- }
+ // First, find all anchors under this scroll container. Can look at positioned
+ // frames' anchor references first, but we want to avoid anchor lookups if we
+ // can.
+ nsTArray<AffectedAnchorGroup> affectedAnchors =
+ FindAnchorsAffectedByScroll(mAnchorPosAnchors, aScrollContainer);
- if (NeedReflowForAnchorPos(anchorFrame, positioned, *data)) {
- // Abspos frames should not affect ancestor intrinsics.
- FrameNeedsReflow(positioned, IntrinsicDirty::None,
- NS_FRAME_HAS_DIRTY_CHILDREN);
- }
+ if (affectedAnchors.IsEmpty()) {
+ return;
+ }
+
+ // Now, update all affected positioned elements' scroll offsets.
+ for (auto* positioned : mAnchorPosPositioned) {
+ const auto scrollDependency =
+ FindScrollCompensatedAnchor(this, affectedAnchors, positioned);
+ if (!scrollDependency) {
+ continue;
+ }
+ const auto& info = scrollDependency->mAnchorInfo;
+ auto& referenceData = scrollDependency->mReferenceData;
+ const auto offset = AnchorPositioningUtils::GetScrollOffsetFor(
+ scrollDependency->mReferenceData.CompensatingForScrollAxes(),
+ positioned, info);
+ if (referenceData.mDefaultScrollShift != offset) {
+ positioned->SetPosition(positioned->GetNormalPosition() - offset);
+ // Update positioned frame's overflow, then the absolute containing
+ // block's.
+ positioned->UpdateOverflow();
+ nsContainerFrame::PlaceFrameView(positioned);
+ positioned->GetParent()->UpdateOverflow();
+ referenceData.mDefaultScrollShift = offset;
}
}
}
diff --git a/layout/base/PresShell.h b/layout/base/PresShell.h
@@ -760,7 +760,8 @@ class PresShell final : public nsStubDocumentObserver,
NeedReflow,
};
AnchorPosUpdateResult UpdateAnchorPosLayout();
- void UpdateAnchorPosLayoutForScroll(ScrollContainerFrame* aScrollContainer);
+ void UpdateAnchorPosForScroll(
+ const ScrollContainerFrame* aScrollContainer) const;
inline void AddAnchorPosPositioned(nsIFrame* aFrame) {
if (!mAnchorPosPositioned.Contains(aFrame)) {
diff --git a/layout/generic/AbsoluteContainingBlock.cpp b/layout/generic/AbsoluteContainingBlock.cpp
@@ -934,6 +934,89 @@ struct MOZ_STACK_CLASS MOZ_RAII AutoFallbackStyleSetter {
OldCacheState mOldCacheState;
};
+struct AnchorShiftInfo {
+ nsPoint mOffset;
+ StylePositionArea mResolvedArea;
+};
+
+struct ContainingBlockRect {
+ Maybe<AnchorShiftInfo> mAnchorShiftInfo = Nothing{};
+ nsRect mRect;
+
+ explicit ContainingBlockRect(const nsRect& aRect) : mRect{aRect} {}
+ ContainingBlockRect(const nsPoint& aOffset,
+ const StylePositionArea& aResolvedArea,
+ const nsRect& aRect)
+ : mAnchorShiftInfo{Some(AnchorShiftInfo{aOffset, aResolvedArea})},
+ mRect{aRect} {}
+
+ StylePositionArea ResolvedPositionArea() const {
+ return mAnchorShiftInfo
+ .map([](const AnchorShiftInfo& aInfo) { return aInfo.mResolvedArea; })
+ .valueOr(StylePositionArea{});
+ }
+};
+
+static nsRect GrowOverflowCheckRect(const nsRect& aOverflowCheckRect,
+ const nsRect& aKidRect,
+ const StylePositionArea& aPosArea) {
+ // The overflow check rect may end up being smaller than the positioned rect -
+ // imagine an absolute containing block & a scroller of the same size, and an
+ // anchor inside it. If position-area: bottom, and the anchor positioned such
+ // that the anchor is touching the lower edge of the containing block & the
+ // scroller, the grid height is 0, which the positioned frame will always
+ // overflow - until the scrollbar moves. To account for this, we will let this
+ // containing block grow in directions that aren't constrained by the anchor.
+ auto result = aOverflowCheckRect;
+ if (aPosArea.first == StylePositionAreaKeyword::Left ||
+ aPosArea.first == StylePositionAreaKeyword::SpanLeft) {
+ // Allowed to grow left
+ if (aKidRect.x < result.x) {
+ result.SetLeftEdge(aKidRect.x);
+ }
+ } else if (aPosArea.first == StylePositionAreaKeyword::Center) {
+ // Not allowed to grow in this axis
+ } else if (aPosArea.first == StylePositionAreaKeyword::Right ||
+ aPosArea.first == StylePositionAreaKeyword::SpanRight) {
+ // Allowed to grow right
+ if (aKidRect.XMost() > aOverflowCheckRect.XMost()) {
+ result.SetRightEdge(aKidRect.XMost());
+ }
+ } else if (aPosArea.first == StylePositionAreaKeyword::SpanAll) {
+ // Allowed to grow in both directions
+ if (aKidRect.x < aOverflowCheckRect.x) {
+ result.SetLeftEdge(aKidRect.x);
+ }
+ if (aKidRect.XMost() > aOverflowCheckRect.XMost()) {
+ result.SetRightEdge(aKidRect.XMost());
+ }
+ }
+ if (aPosArea.first == StylePositionAreaKeyword::Top ||
+ aPosArea.first == StylePositionAreaKeyword::SpanTop) {
+ // Allowed to grow up
+ if (aKidRect.y < aOverflowCheckRect.y) {
+ result.SetTopEdge(aKidRect.y);
+ }
+ } else if (aPosArea.first == StylePositionAreaKeyword::Center) {
+ // Not allowed to grow in this axis
+ } else if (aPosArea.first == StylePositionAreaKeyword::Bottom ||
+ aPosArea.first == StylePositionAreaKeyword::SpanBottom) {
+ // Allowed to grow down
+ if (aKidRect.YMost() > aOverflowCheckRect.YMost()) {
+ result.SetBottomEdge(aKidRect.YMost());
+ }
+ } else if (aPosArea.first == StylePositionAreaKeyword::SpanAll) {
+ // Allowed to grow in both directions
+ if (aKidRect.y < aOverflowCheckRect.y) {
+ result.SetTopEdge(aKidRect.y);
+ }
+ if (aKidRect.YMost() > aOverflowCheckRect.YMost()) {
+ result.SetBottomEdge(aKidRect.YMost());
+ }
+ }
+ return result;
+}
+
// XXX Optimize the case where it's a resize reflow and the absolutely
// positioned child has the exact same size and position and skip the
// reflow...
@@ -1035,14 +1118,13 @@ void AbsoluteContainingBlock::ReflowAbsoluteFrame(
AutoFallbackStyleSetter fallback(aKidFrame, currentFallbackStyle,
aAnchorPosResolutionCache,
firstTryIndex == currentFallbackIndex);
- auto positionArea = aKidFrame->StylePosition()->mPositionArea;
- StylePositionArea resolvedPositionArea;
- const nsRect usedCb = [&] {
+ const auto cb = [&]() {
if (isGrid) {
// TODO(emilio): how does position-area interact with grid?
- return nsGridContainerFrame::GridItemCB(aKidFrame);
+ return ContainingBlockRect{nsGridContainerFrame::GridItemCB(aKidFrame)};
}
+ auto positionArea = aKidFrame->StylePosition()->mPositionArea;
if (currentFallback && currentFallback->IsPositionArea()) {
MOZ_ASSERT(currentFallback->IsPositionArea());
positionArea = currentFallback->AsPositionArea();
@@ -1054,29 +1136,45 @@ void AbsoluteContainingBlock::ReflowAbsoluteFrame(
aKidFrame, aDelegatingFrame, nullptr, false,
aAnchorPosResolutionCache);
if (defaultAnchorInfo) {
- return AnchorPositioningUtils::
+ // Offset should be up to, but not including the containing block's
+ // scroll offset.
+ const auto offset = AnchorPositioningUtils::GetScrollOffsetFor(
+ aAnchorPosResolutionCache->mReferenceData
+ ->CompensatingForScrollAxes(),
+ aKidFrame, aAnchorPosResolutionCache->mDefaultAnchorCache);
+ // Imagine an abspos container with a scroller in it, and then an
+ // anchor in it, where the anchor is visually in the middle of the
+ // scrollport. Then, when the scroller moves such that the anchor's
+ // left edge is on that of the scrollports, w.r.t. containing block,
+ // the anchor is zero left offset horizontally. The position-area grid
+ // needs to account for this.
+ const auto scrolledAnchorRect = defaultAnchorInfo->mRect - offset;
+ StylePositionArea resolvedPositionArea{};
+ const auto scrolledAnchorCb = AnchorPositioningUtils::
AdjustAbsoluteContainingBlockRectForPositionArea(
- defaultAnchorInfo->mRect, aOriginalContainingBlockRect,
+ scrolledAnchorRect, aOriginalContainingBlockRect,
aKidFrame->GetWritingMode(),
aDelegatingFrame->GetWritingMode(), positionArea,
&resolvedPositionArea);
+ return ContainingBlockRect{offset, resolvedPositionArea,
+ scrolledAnchorCb};
}
}
if (ViewportFrame* viewport = do_QueryFrame(aDelegatingFrame)) {
if (!IsSnapshotContainingBlock(aKidFrame)) {
- return viewport->GetContainingBlockAdjustedForScrollbars(
- aReflowInput);
+ return ContainingBlockRect{
+ viewport->GetContainingBlockAdjustedForScrollbars(aReflowInput)};
}
- return dom::ViewTransition::SnapshotContainingBlockRect(
- viewport->PresContext());
+ return ContainingBlockRect{
+ dom::ViewTransition::SnapshotContainingBlockRect(
+ viewport->PresContext())};
}
- return aOriginalContainingBlockRect;
+ return ContainingBlockRect{aOriginalContainingBlockRect};
}();
-
const WritingMode outerWM = aReflowInput.GetWritingMode();
const WritingMode wm = aKidFrame->GetWritingMode();
- const LogicalSize cbSize(outerWM, usedCb.Size());
+ const LogicalSize cbSize(outerWM, cb.mRect.Size());
ReflowInput::InitFlags initFlags;
const bool staticPosIsCBOrigin = [&] {
@@ -1124,7 +1222,7 @@ void AbsoluteContainingBlock::ReflowAbsoluteFrame(
// Don't split things below the fold. (Ideally we shouldn't *have*
// anything totally below the fold, but we can't position frames
// across next-in-flow breaks yet.
- (aKidFrame->GetLogicalRect(usedCb.Size()).BStart(wm) <=
+ (aKidFrame->GetLogicalRect(cb.mRect.Size()).BStart(wm) <=
aReflowInput.AvailableBSize());
// Get the border values
@@ -1179,7 +1277,7 @@ void AbsoluteContainingBlock::ReflowAbsoluteFrame(
// If we're solving for start in either inline or block direction,
// then compute it now that we know the dimensions.
ResolveSizeDependentOffsets(kidReflowInput, cbSize, kidSize, margin,
- resolvedPositionArea, offsets);
+ cb.ResolvedPositionArea(), offsets);
if (kidReflowInput.mFlags.mDeferAutoMarginComputation) {
ResolveAutoMarginsAfterLayout(kidReflowInput, cbSize, kidSize, margin,
@@ -1232,7 +1330,7 @@ void AbsoluteContainingBlock::ReflowAbsoluteFrame(
offsets.IStart(outerWM),
offsets.IEnd(outerWM),
}),
- resolvedPositionArea);
+ cb.ResolvedPositionArea());
offsets.IStart(outerWM) += alignOffset;
offsets.IEnd(outerWM) =
@@ -1250,7 +1348,7 @@ void AbsoluteContainingBlock::ReflowAbsoluteFrame(
offsets.BStart(outerWM),
offsets.BEnd(outerWM),
}),
- resolvedPositionArea);
+ cb.ResolvedPositionArea());
offsets.BStart(outerWM) += alignOffset;
offsets.BEnd(outerWM) =
cbSize.BSize(outerWM) -
@@ -1267,7 +1365,11 @@ void AbsoluteContainingBlock::ReflowAbsoluteFrame(
border.Size(outerWM).GetPhysicalSize(outerWM));
// Offset the frame rect by the given origin of the absolute CB.
- r += usedCb.TopLeft();
+ r += cb.mRect.TopLeft();
+ if (cb.mAnchorShiftInfo) {
+ // Push the frame out to where the anchor is.
+ r += cb.mAnchorShiftInfo->mOffset;
+ }
aKidFrame->SetRect(r);
@@ -1284,8 +1386,60 @@ void AbsoluteContainingBlock::ReflowAbsoluteFrame(
aKidFrame->DidReflow(aPresContext, &kidReflowInput);
- const bool fits =
- aStatus.IsComplete() && usedCb.Contains(aKidFrame->GetRect());
+ [&]() {
+ if (!aAnchorPosResolutionCache) {
+ return;
+ }
+ auto* referenceData = aAnchorPosResolutionCache->mReferenceData;
+ if (referenceData->CompensatingForScrollAxes().isEmpty()) {
+ return;
+ }
+ // Now that all the anchor-related values are resolved, completing the
+ // scroll compensation flag, compute the scroll offsets.
+ const auto offset = [&]() {
+ if (cb.mAnchorShiftInfo) {
+ // Already resolved.
+ return cb.mAnchorShiftInfo->mOffset;
+ }
+ return AnchorPositioningUtils::GetScrollOffsetFor(
+ referenceData->CompensatingForScrollAxes(), aKidFrame,
+ aAnchorPosResolutionCache->mDefaultAnchorCache);
+ }();
+ // Apply the hypothetical scroll offset.
+ const auto position = aKidFrame->GetPosition();
+ // Set initial scroll position. TODO(dshin, bug 1987962): Need
+ // additional work for remembered scroll offset here.
+ aKidFrame->SetProperty(nsIFrame::NormalPositionProperty(), position);
+ if (offset != nsPoint{}) {
+ aKidFrame->SetPosition(position - offset);
+ // Ensure that the positioned frame's overflow is updated. Absolutely
+ // containing block's overflow will be updated shortly below.
+ aKidFrame->UpdateOverflow();
+ nsContainerFrame::PlaceFrameView(aKidFrame);
+ }
+ aAnchorPosResolutionCache->mReferenceData->mDefaultScrollShift = offset;
+ }();
+
+ const auto fits = aStatus.IsComplete() && [&]() {
+ // TODO(dshin, bug 1996832): This should probably be done at call sites of
+ // `AbsoluteContainingBlock::Reflow`.
+ const auto paddingEdgeShift = [&]() {
+ const auto border = aDelegatingFrame->GetUsedBorder();
+ return nsPoint{border.left, border.top};
+ }();
+ auto overflowCheckRect = cb.mRect + paddingEdgeShift;
+ if (aAnchorPosResolutionCache && cb.mAnchorShiftInfo) {
+ overflowCheckRect =
+ GrowOverflowCheckRect(overflowCheckRect, aKidFrame->GetNormalRect(),
+ cb.mAnchorShiftInfo->mResolvedArea);
+ const auto originalContainingBlockRect =
+ aOriginalContainingBlockRect + paddingEdgeShift;
+ return AnchorPositioningUtils::FitsInContainingBlock(
+ overflowCheckRect, originalContainingBlockRect,
+ aKidFrame->GetRect());
+ }
+ return overflowCheckRect.Contains(aKidFrame->GetRect());
+ }();
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.
diff --git a/layout/generic/ScrollContainerFrame.cpp b/layout/generic/ScrollContainerFrame.cpp
@@ -3270,7 +3270,7 @@ void ScrollContainerFrame::ScrollToImpl(
return;
}
}
- PresShell()->UpdateAnchorPosLayoutForScroll(this);
+ PresShell()->UpdateAnchorPosForScroll(this);
presContext->RecordInteractionTime(
nsPresContext::InteractionType::ScrollInteraction, TimeStamp::Now());
diff --git a/layout/generic/nsIFrame.cpp b/layout/generic/nsIFrame.cpp
@@ -796,6 +796,7 @@ void nsIFrame::HandlePrimaryFrameStyleChange(ComputedStyle* aOldStyle) {
const bool isReferringToAnchor = HasAnchorPosReference();
if (wasReferringToAnchor && !isReferringToAnchor) {
PresShell()->RemoveAnchorPosPositioned(this);
+ RemoveProperty(NormalPositionProperty());
} else if (!wasReferringToAnchor && isReferringToAnchor) {
PresShell()->AddAnchorPosPositioned(this);
}
@@ -8404,8 +8405,6 @@ void nsIFrame::MovePositionBy(const nsPoint& aTranslation) {
}
nsRect nsIFrame::GetNormalRect() const {
- // It might be faster to first check
- // StyleDisplay()->IsRelativelyPositionedStyle().
bool hasProperty;
nsPoint normalPosition = GetProperty(NormalPositionProperty(), &hasProperty);
if (hasProperty) {
@@ -8473,13 +8472,14 @@ OverflowAreas nsIFrame::GetOverflowAreasRelativeToParent() const {
OverflowAreas nsIFrame::GetActualAndNormalOverflowAreasRelativeToParent()
const {
- if (MOZ_LIKELY(!IsRelativelyOrStickyPositioned())) {
+ const bool hasAnchorPosReference = HasAnchorPosReference();
+ if (MOZ_LIKELY(!IsRelativelyOrStickyPositioned() && !hasAnchorPosReference)) {
return GetOverflowAreasRelativeToParent();
}
const OverflowAreas overflows = GetOverflowAreas();
OverflowAreas actualAndNormalOverflows = overflows + GetNormalPosition();
- if (IsRelativelyPositioned()) {
+ if (IsRelativelyPositioned() || hasAnchorPosReference) {
actualAndNormalOverflows.UnionWith(overflows + GetPosition());
} else {
// For sticky positioned elements, we only use the normal position for the