commit 3dd95597854d8077174b6a03ba5f021dade6979d parent f52055643f402a739ac3649b90949aaf288711d9 Author: David Shin <dshin@mozilla.com> Date: Mon, 22 Dec 2025 21:00:41 +0000 Bug 2004495: Use inset-modified containing block for overflow checks. r=layout-anchor-positioning-reviewers,firefox-style-system-reviewers,layout-reviewers,emilio We now keep track of: * Resolved insets * Which side(s) is(are) scroll-compensated Which are then used on modified containing block to yield the final containing block that takes inset & scrolling (Which affects the anchor's relative position w.r.t. the containing block). Differential Revision: https://phabricator.services.mozilla.com/D276679 Diffstat:
13 files changed, 158 insertions(+), 80 deletions(-)
diff --git a/layout/base/AnchorPositioningUtils.cpp b/layout/base/AnchorPositioningUtils.cpp @@ -875,8 +875,42 @@ bool AnchorPositioningUtils::FitsInContainingBlock( const nsIFrame* aPositioned, const AnchorPosReferenceData& aReferenceData) { MOZ_ASSERT(aPositioned->GetProperty(nsIFrame::AnchorPosReferences()) == &aReferenceData); - return aReferenceData.mOriginalContainingBlockRect.Contains( - aPositioned->GetMarginRect()); + + const auto& scrollShift = aReferenceData.mDefaultScrollShift; + const auto scrollCompensatedSides = aReferenceData.mScrollCompensatedSides; + nsSize checkSize = [&]() { + const auto& adjustedCB = aReferenceData.mAdjustedContainingBlock; + if (scrollShift == nsPoint{} || scrollCompensatedSides == SideBits::eNone) { + return adjustedCB.Size(); + } + + // We now know that this frame's anchor has moved in relation to + // the original containing block, and that at least one side of our + // IMCB is attached to it. + + // Scroll shift the adjusted containing block. + const auto shifted = aReferenceData.mAdjustedContainingBlock - scrollShift; + const auto& originalCB = aReferenceData.mOriginalContainingBlockRect; + + // Now, move edges that are not attached to the anchors and pin it + // to the original containing block. + const nsPoint pt{ + scrollCompensatedSides & SideBits::eLeft ? shifted.X() : originalCB.X(), + scrollCompensatedSides & SideBits::eTop ? shifted.Y() : originalCB.Y()}; + const nsPoint ptMost{ + scrollCompensatedSides & SideBits::eRight ? shifted.XMost() + : originalCB.XMost(), + scrollCompensatedSides & SideBits::eBottom ? shifted.YMost() + : originalCB.YMost()}; + + return nsSize{ptMost.x - pt.x, ptMost.y - pt.y}; + }(); + + // Finally, reduce by inset. + checkSize -= nsSize{aReferenceData.mInsets.LeftRight(), + aReferenceData.mInsets.TopBottom()}; + + return aPositioned->GetMarginRectRelativeToSelf().Size() <= checkSize; } nsIFrame* AnchorPositioningUtils::GetAnchorThatFrameScrollsWith( diff --git a/layout/base/AnchorPositioningUtils.h b/layout/base/AnchorPositioningUtils.h @@ -95,6 +95,8 @@ class AnchorPosReferenceData { mozilla::PhysicalAxes mCompensatingForScroll; nsPoint mDefaultScrollShift; nsRect mAdjustedContainingBlock; + SideBits mScrollCompensatedSides; + nsMargin mInsets; }; using Value = mozilla::Maybe<AnchorPosResolutionData>; @@ -130,13 +132,19 @@ class AnchorPosReferenceData { auto compensatingForScroll = std::exchange(mCompensatingForScroll, {}); auto defaultScrollShift = std::exchange(mDefaultScrollShift, {}); auto adjustedContainingBlock = std::exchange(mAdjustedContainingBlock, {}); - return {compensatingForScroll, defaultScrollShift, adjustedContainingBlock}; + auto containingBlockSidesAttachedToAnchor = + std::exchange(mScrollCompensatedSides, SideBits::eNone); + auto insets = std::exchange(mInsets, nsMargin{}); + return {compensatingForScroll, defaultScrollShift, adjustedContainingBlock, + containingBlockSidesAttachedToAnchor, insets}; } void UndoTryPositionWithSameDefaultAnchor(PositionTryBackup&& aBackup) { mCompensatingForScroll = aBackup.mCompensatingForScroll; mDefaultScrollShift = aBackup.mDefaultScrollShift; mAdjustedContainingBlock = aBackup.mAdjustedContainingBlock; + mScrollCompensatedSides = aBackup.mScrollCompensatedSides; + mInsets = aBackup.mInsets; } // Distance from the default anchor to the nearest scroll container. @@ -154,6 +162,25 @@ class AnchorPosReferenceData { // Name of the default used anchor. Not necessarily positioned frame's // style, because of fallbacks. RefPtr<const nsAtom> mDefaultAnchorName; + // Flag indicating which sides of the containing block attach to the + // scroll-compensated anchor. Whenever a scroll-compensated anchor scrolls, it + // effectively moves around w.r.t. its absolute containing block. This + // effectively changes the size of the containing block. For example, given: + // + // * Absolute containing block of 50px height, + // * Scroller, under the abs CB, with the scrolled content height of 100px, + // * Anchor element, under the scroller, of 30px height, and + // * Positioned element of 30px height, attached to anchor at the bottom. + // + // The positioned element would overflow the abs CB, until the scroller moves + // down by 10px. We address this by defining sides of the CB that scrolls + // with the anchor, so that whenever we carry out an overflow check, we move + // those sides by the scroll offset, while pinning the rest of the sides to + // the original containing block. + SideBits mScrollCompensatedSides = SideBits::eNone; + // Resolved insets for this positioned element. Modifies the adjusted & + // scrolled containing block. + nsMargin mInsets; private: ResolutionMap mMap; diff --git a/layout/generic/AbsoluteContainingBlock.cpp b/layout/generic/AbsoluteContainingBlock.cpp @@ -1123,6 +1123,36 @@ struct ContainingBlockRect { } }; +static SideBits GetScrollCompensatedSidesFor( + const StylePositionArea& aPositionArea) { + SideBits sides{SideBits::eNone}; + // The opposite side of the direction keyword is attached to the + // position-anchor grid, which is then attached to the anchor, and so is + // scroll compensated. `center` is constrained by the position-area grid + // on both sides. `span-all` is unconstrained in that axis. + if (aPositionArea.first == StylePositionAreaKeyword::Left || + aPositionArea.first == StylePositionAreaKeyword::SpanLeft) { + sides |= SideBits::eRight; + } else if (aPositionArea.first == StylePositionAreaKeyword::Right || + aPositionArea.first == StylePositionAreaKeyword::SpanRight) { + sides |= SideBits::eLeft; + } else if (aPositionArea.first == StylePositionAreaKeyword::Center) { + sides |= SideBits::eLeftRight; + } + + if (aPositionArea.second == StylePositionAreaKeyword::Top || + aPositionArea.second == StylePositionAreaKeyword::SpanTop) { + sides |= SideBits::eBottom; + } else if (aPositionArea.second == StylePositionAreaKeyword::Bottom || + aPositionArea.second == StylePositionAreaKeyword::SpanBottom) { + sides |= SideBits::eTop; + } else if (aPositionArea.first == StylePositionAreaKeyword::Center) { + sides |= SideBits::eTopBottom; + } + + return sides; +} + // 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... @@ -1265,6 +1295,10 @@ void AbsoluteContainingBlock::ReflowAbsoluteFrame( containingBlock, aKidFrame->GetWritingMode(), aDelegatingFrame->GetWritingMode(), positionArea, &resolvedPositionArea); + // By definition, we're using the default anchor, and are scroll + // compensated. + aAnchorPosResolutionCache->mReferenceData->mScrollCompensatedSides = + GetScrollCompensatedSidesFor(resolvedPositionArea); return ContainingBlockRect{ offset, resolvedPositionArea, aOriginalScrollableContainingBlockRect, @@ -1400,6 +1434,7 @@ void AbsoluteContainingBlock::ReflowAbsoluteFrame( ReflowOutput kidDesiredSize(kidReflowInput); aKidFrame->Reflow(aPresContext, kidDesiredSize, kidReflowInput, aStatus); + nsMargin insets; if (aKidFrame->IsMenuPopupFrame()) { // Do nothing. Popup frame will handle its own positioning. } else if (kidPrevInFlow) { @@ -1477,6 +1512,26 @@ void AbsoluteContainingBlock::ReflowAbsoluteFrame( const auto* placeholderContainer = GetPlaceholderContainer(kidReflowInput.mFrame); + insets = [&]() { + auto result = offsets; + // Zero out weaker insets, if one exists - This offset gets forced to + // the margin edge of the child on that side, and for the purposes of + // overflow checks, we consider them to be zero. + if (iStartInsetAuto && !iEndInsetAuto) { + result.IStart(outerWM) = 0; + } else if (iInsetAuto) { + result.IEnd(outerWM) = 0; + } + if (bStartInsetAuto && !bEndInsetAuto) { + result.BStart(outerWM) = 0; + } else if (bInsetAuto) { + result.BEnd(outerWM) = 0; + } + return result.GetPhysicalMargin(outerWM); + }(); + if (aAnchorPosResolutionCache) { + aAnchorPosResolutionCache->mReferenceData->mInsets = insets; + } if (!iInsetAuto) { MOZ_ASSERT( !kidReflowInput.mFlags.mIOffsetsNeedCSSAlign, @@ -1565,11 +1620,20 @@ void AbsoluteContainingBlock::ReflowAbsoluteFrame( aAnchorPosResolutionCache->mReferenceData->mDefaultScrollShift = offset; }(); + const auto FitsInContainingBlock = [&]() { + if (aAnchorPosResolutionCache) { + return AnchorPositioningUtils::FitsInContainingBlock( + aKidFrame, *aAnchorPosResolutionCache->mReferenceData); + } + auto imcbSize = cb.mFinalRect.Size(); + imcbSize -= nsSize{insets.LeftRight(), insets.TopBottom()}; + return aKidFrame->GetMarginRectRelativeToSelf().Size() <= imcbSize; + }; + // FIXME(bug 2004495): Per spec this should be the inset-modified // containing-block, see: // https://drafts.csswg.org/css-anchor-position-1/#fallback-apply - const auto fits = aStatus.IsComplete() && cb.mMaybeScrollableRect.Contains( - aKidFrame->GetMarginRect()); + const auto fits = aStatus.IsComplete() && FitsInContainingBlock(); 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/style/GeckoBindings.cpp b/layout/style/GeckoBindings.cpp @@ -1906,23 +1906,30 @@ bool Gecko_GetAnchorPosOffset(const AnchorPosOffsetResolutionParams* aParams, if (!info) { return false; } - if (info->mCompensatesForScroll && cache) { - // Without cache (Containing information on default anchor) being available, - // we woudln't be able to determine scroll compensation status. - const auto axis = [aPropSide]() { - switch (aPropSide) { - case StylePhysicalSide::Left: - case StylePhysicalSide::Right: - return PhysicalAxis::Horizontal; - case StylePhysicalSide::Top: - case StylePhysicalSide::Bottom: - break; - default: - MOZ_ASSERT_UNREACHABLE("Unhandled side?"); - } - return PhysicalAxis::Vertical; - }(); - cache->mReferenceData->AdjustCompensatingForScroll(axis); + if (cache) { + // Cache is set during reflow, which is really the only time we want to + // actively modify scroll compensation state & side. + if (info->mCompensatesForScroll) { + const auto axis = [aPropSide]() { + switch (aPropSide) { + case StylePhysicalSide::Left: + case StylePhysicalSide::Right: + return PhysicalAxis::Horizontal; + case StylePhysicalSide::Top: + case StylePhysicalSide::Bottom: + break; + default: + MOZ_ASSERT_UNREACHABLE("Unhandled side?"); + } + return PhysicalAxis::Vertical; + }(); + cache->mReferenceData->AdjustCompensatingForScroll(axis); + // Non scroll-compensated anchor will not have any impact on the + // containing block due to scrolling. See documentation for + // `mScrollCompensatedSides`. + cache->mReferenceData->mScrollCompensatedSides |= + SideToSideBit(ToSide(aPropSide)); + } } // Compute the offset here in C++, where translating between physical/logical // coordinates is easier. diff --git a/testing/web-platform/meta/css/css-anchor-position/anchor-scroll-position-try-007.html.ini b/testing/web-platform/meta/css/css-anchor-position/anchor-scroll-position-try-007.html.ini @@ -4,7 +4,3 @@ if os == "android": [PASS, FAIL] FAIL - [Should use the last position option initially] - expected: - if os == "android": [PASS, FAIL] - FAIL diff --git a/testing/web-platform/meta/css/css-anchor-position/anchor-scroll-position-try-008.html.ini b/testing/web-platform/meta/css/css-anchor-position/anchor-scroll-position-try-008.html.ini @@ -1,5 +0,0 @@ -[anchor-scroll-position-try-008.html] - [Should use the last fallback position initially] - expected: - if os == "android": [PASS, FAIL] - FAIL diff --git a/testing/web-platform/meta/css/css-anchor-position/anchor-scroll-position-try-010.html.ini b/testing/web-platform/meta/css/css-anchor-position/anchor-scroll-position-try-010.html.ini @@ -6,8 +6,3 @@ [Should use the first fallback position with enough space right and above] expected: if (os == "mac") or (os == "android"): [PASS, FAIL] - - [Should use the last fallback position initially] - expected: - if os == "android": PASS - FAIL diff --git a/testing/web-platform/meta/css/css-anchor-position/position-try-002.html.ini b/testing/web-platform/meta/css/css-anchor-position/position-try-002.html.ini @@ -1,3 +0,0 @@ -[position-try-002.html] - [.target 1] - expected: FAIL diff --git a/testing/web-platform/meta/css/css-anchor-position/position-try-003.html.ini b/testing/web-platform/meta/css/css-anchor-position/position-try-003.html.ini @@ -1,9 +0,0 @@ -[position-try-003.html] - [.anchored 1] - expected: FAIL - - [.anchored 2] - expected: FAIL - - [.anchored 3] - expected: FAIL diff --git a/testing/web-platform/meta/css/css-anchor-position/position-try-container-query.html.ini b/testing/web-platform/meta/css/css-anchor-position/position-try-container-query.html.ini @@ -1,3 +0,0 @@ -[position-try-container-query.html] - [Size container query responds to fallback width and applies height to not fit the first fallback] - expected: FAIL diff --git a/testing/web-platform/meta/css/css-anchor-position/position-try-fallbacks-003.html.ini b/testing/web-platform/meta/css/css-anchor-position/position-try-fallbacks-003.html.ini @@ -10,21 +10,13 @@ FAIL [scroll to 101] - expected: - FAIL + expected: FAIL [scroll back to 100] - expected: - FAIL + expected: FAIL [redisplay at 100] - expected: - FAIL - - [scroll to 300] - expected: - FAIL + expected: FAIL [scroll back to 0] - expected: - FAIL + expected: FAIL diff --git a/testing/web-platform/meta/css/css-anchor-position/position-try-fallbacks-004.html.ini b/testing/web-platform/meta/css/css-anchor-position/position-try-fallbacks-004.html.ini @@ -1,2 +0,0 @@ -[position-try-fallbacks-004.html] - expected: FAIL diff --git a/testing/web-platform/meta/css/css-anchor-position/position-try-tree-scoped.html.ini b/testing/web-platform/meta/css/css-anchor-position/position-try-tree-scoped.html.ini @@ -1,27 +1,12 @@ [position-try-tree-scoped.html] - [@position-try from same scope as :host rule] - expected: FAIL - - [@position-try from same scope as ::slotted() rule] - expected: FAIL - [@position-try from same scope as ::part() rule] expected: FAIL - [Document position-try-fallbacks matches @position-try in document scope] - expected: FAIL - [Outer position-try-fallbacks matches @position-try in document scope] expected: FAIL - [Outer position-try-fallbacks matches @position-try in #outer_host scope] - expected: FAIL - [Inner position-try-fallbacks matches @position-try in document scope] expected: FAIL [Inner position-try-fallbacks matches @position-try in #outer_host scope] expected: FAIL - - [Inner position-try-fallbacks matches @position-try in #inner_host scope] - expected: FAIL