tor-browser

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

commit 878edfaa957881203ae1f443eefd2de914232590
parent e83f5939e36abcb07415d85c3b04f5c3db05589a
Author: David Shin <dshin@mozilla.com>
Date:   Thu, 30 Oct 2025 15:44:26 +0000

Bug 1987963: Apply default scroll offset on scroll. r=layout-reviewers,tnikkel,jwatt

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

Diffstat:
Mlayout/base/AnchorPositioningUtils.cpp | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlayout/base/AnchorPositioningUtils.h | 26+++++++++++++++++++++-----
Mlayout/base/PresShell.cpp | 265+++++++++++++++++++++++++++++++++++++------------------------------------------
Mlayout/base/PresShell.h | 3++-
Mlayout/generic/AbsoluteContainingBlock.cpp | 194++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Mlayout/generic/ScrollContainerFrame.cpp | 2+-
Mlayout/generic/nsIFrame.cpp | 8++++----
7 files changed, 381 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,11 @@ const nsIFrame* AnchorPositioningUtils::GetAnchorPosImplicitAnchor( : pseudoRootFrame->GetParent(); } +bool AnchorPositioningUtils::FitsInContainingBlock( + const nsRect& aOverflowCheckRect, + const nsRect& aOriginalContainingBlockSize, const nsRect& aRect) { + return aOverflowCheckRect.Intersect(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