commit ee48e485917c6f256be21b2820d32ff91a5955e6
parent 3b30c384711efe66e18ad027c0312b22baba1c62
Author: David Shin <dshin@mozilla.com>
Date: Thu, 11 Dec 2025 00:07:52 +0000
Bug 2003591: Resolve anchor() against modified CB. r=layout-anchor-positioning-reviewers,firefox-style-system-reviewers,layout-reviewers,emilio
Differential Revision: https://phabricator.services.mozilla.com/D275663
Diffstat:
6 files changed, 141 insertions(+), 18 deletions(-)
diff --git a/layout/base/AnchorPositioningUtils.cpp b/layout/base/AnchorPositioningUtils.cpp
@@ -872,7 +872,7 @@ bool AnchorPositioningUtils::FitsInContainingBlock(
const nsIFrame* aPositioned, const AnchorPosReferenceData& aReferenceData) {
MOZ_ASSERT(aPositioned->GetProperty(nsIFrame::AnchorPosReferences()) ==
&aReferenceData);
- return aReferenceData.mContainingBlockRect.Contains(
+ return aReferenceData.mOriginalContainingBlockRect.Contains(
aPositioned->GetMarginRect());
}
diff --git a/layout/base/AnchorPositioningUtils.h b/layout/base/AnchorPositioningUtils.h
@@ -92,6 +92,7 @@ class AnchorPosReferenceData {
struct PositionTryBackup {
mozilla::PhysicalAxes mCompensatingForScroll;
nsPoint mDefaultScrollShift;
+ nsRect mAdjustedContainingBlock;
};
using Value = mozilla::Maybe<AnchorPosResolutionData>;
@@ -126,21 +127,26 @@ class AnchorPosReferenceData {
PositionTryBackup TryPositionWithSameDefaultAnchor() {
auto compensatingForScroll = std::exchange(mCompensatingForScroll, {});
auto defaultScrollShift = std::exchange(mDefaultScrollShift, {});
- return {compensatingForScroll, defaultScrollShift};
+ auto adjustedContainingBlock = std::exchange(mAdjustedContainingBlock, {});
+ return {compensatingForScroll, defaultScrollShift, adjustedContainingBlock};
}
void UndoTryPositionWithSameDefaultAnchor(PositionTryBackup&& aBackup) {
mCompensatingForScroll = aBackup.mCompensatingForScroll;
mDefaultScrollShift = aBackup.mDefaultScrollShift;
+ mAdjustedContainingBlock = aBackup.mAdjustedContainingBlock;
}
// Distance from the default anchor to the nearest scroll container.
DistanceToNearestScrollContainer mDistanceToDefaultScrollContainer;
// https://drafts.csswg.org/css-anchor-position-1/#default-scroll-shift
nsPoint mDefaultScrollShift;
- // Rect of containing block before being inset-modified, at the time of
- // resolution.
- nsRect mContainingBlockRect;
+ // Rect of the original containg block.
+ nsRect mOriginalContainingBlockRect;
+ // Adjusted containing block, by position-area or grid, as per
+ // https://drafts.csswg.org/css-position/#original-cb
+ // TODO(dshin, bug 2004596): "or" should be "and/or."
+ nsRect mAdjustedContainingBlock;
// TODO(dshin, bug 1987962): Remembered scroll offset
// https://drafts.csswg.org/css-anchor-position-1/#remembered-scroll-offset
// Name of the default used anchor. Not necessarily positioned frame's
diff --git a/layout/generic/AbsoluteContainingBlock.cpp b/layout/generic/AbsoluteContainingBlock.cpp
@@ -1279,8 +1279,15 @@ void AbsoluteContainingBlock::ReflowAbsoluteFrame(
return ContainingBlockRect{aOriginalContainingBlockRect};
}();
if (aAnchorPosResolutionCache) {
- aAnchorPosResolutionCache->mReferenceData->mContainingBlockRect =
- cb.mMaybeScrollableRect;
+ const auto& originalCb = cb.mMaybeScrollableRect;
+ aAnchorPosResolutionCache->mReferenceData->mOriginalContainingBlockRect =
+ originalCb;
+ // Stash the adjusted containing block as well, since the insets need to
+ // resolve against the adjusted CB, e.g. With `position-area: bottom
+ // right;`, + `left: anchor(right);`
+ // resolves to 0.
+ aAnchorPosResolutionCache->mReferenceData->mAdjustedContainingBlock =
+ cb.mFinalRect;
}
const WritingMode outerWM = aReflowInput.GetWritingMode();
const WritingMode wm = aKidFrame->GetWritingMode();
diff --git a/layout/style/GeckoBindings.cpp b/layout/style/GeckoBindings.cpp
@@ -1897,13 +1897,13 @@ bool Gecko_GetAnchorPosOffset(const AnchorPosOffsetResolutionParams* aParams,
}
const auto* positioned = aParams->mBaseParams.mFrame;
const auto* containingBlock = positioned->GetParent();
+ auto* cache = aParams->mBaseParams.mCache;
const auto info = AnchorPositioningUtils::ResolveAnchorPosRect(
- positioned, containingBlock, aAnchorName, !aParams->mCBSize,
- aParams->mBaseParams.mCache);
+ positioned, containingBlock, aAnchorName, !aParams->mCBSize, cache);
if (!info) {
return false;
}
- if (info->mCompensatesForScroll && aParams->mBaseParams.mCache) {
+ 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]() {
@@ -1919,19 +1919,38 @@ bool Gecko_GetAnchorPosOffset(const AnchorPosOffsetResolutionParams* aParams,
}
return PhysicalAxis::Vertical;
}();
- aParams->mBaseParams.mCache->mReferenceData->AdjustCompensatingForScroll(
- axis);
+ cache->mReferenceData->AdjustCompensatingForScroll(axis);
}
// Compute the offset here in C++, where translating between physical/logical
// coordinates is easier.
- const auto& rect = info->mRect;
+
const auto usesCBWM = AnchorSideUsesCBWM(aAnchorSideKeyword);
const auto cbwm = containingBlock->GetWritingMode();
const auto wm =
usesCBWM ? aParams->mBaseParams.mFrame->GetWritingMode() : cbwm;
- const auto logicalCBSize = aParams->mCBSize
- ? aParams->mCBSize->ConvertTo(wm, cbwm)
- : containingBlock->PaddingSize(wm);
+ const auto [rect, logicalCBSize] = [&] {
+ // We need `AnchorPosReferenceData` to compute the anchor offset against
+ // the adjusted CB, so make the best attempt to retrieve it.
+ // TODO(dshin, bug 2005207): We really need to unify containing block
+ // lookups and clean up cache lookups here.
+ const auto* referenceData =
+ cache ? cache->mReferenceData
+ : positioned->GetProperty(nsIFrame::AnchorPosReferences());
+ if (!referenceData) {
+ return std::make_pair(
+ info->mRect, aParams->mCBSize ? aParams->mCBSize->ConvertTo(wm, cbwm)
+ : containingBlock->PaddingSize(wm));
+ }
+ // Offset happens from padding rect.
+ const auto offset = referenceData->mAdjustedContainingBlock.TopLeft() -
+ referenceData->mOriginalContainingBlockRect.TopLeft();
+ return std::make_pair(
+ info->mRect - offset,
+ aParams->mCBSize
+ ? aParams->mCBSize->ConvertTo(wm, cbwm)
+ : LogicalSize{cbwm, referenceData->mAdjustedContainingBlock.Size()}
+ .ConvertTo(wm, cbwm));
+ }();
const LogicalRect logicalAnchorRect{wm, rect,
logicalCBSize.GetPhysicalSize(wm)};
const auto logicalPropSide = wm.LogicalSideForPhysicalSide(ToSide(aPropSide));
diff --git a/testing/web-platform/meta/css/css-anchor-position/position-area-anchor-001.html.ini b/testing/web-platform/meta/css/css-anchor-position/position-area-anchor-001.html.ini
@@ -1,2 +0,0 @@
-[position-area-anchor-001.html]
- expected: FAIL
diff --git a/testing/web-platform/tests/css/css-anchor-position/position-area-anchor-getComputedStyle.html b/testing/web-platform/tests/css/css-anchor-position/position-area-anchor-getComputedStyle.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests that getComputedStyle() resolves anchor() functions w.r.t. position-area containing block</title>
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#position-area">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#anchor-pos">
+<link rel="author" name="David Shin" href="mailto:dshin@mozilla.com">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+.abs-cb {
+ width: 200px;
+ height: 200px;
+ border: 5px solid;
+ position: relative;
+}
+
+.anchor {
+ anchor-name: --a;
+ width: 50px;
+ height: 50px;
+ margin-left: 75px;
+ background: pink;
+}
+
+.positioned {
+ position: absolute;
+ position-anchor: --a;
+ width: 25px;
+ height: 25px;
+ background: purple;
+}
+
+.top {
+ bottom: anchor(top);
+}
+
+.bottom {
+ top: anchor(bottom);
+}
+
+.left {
+ right: anchor(left);
+}
+
+.right {
+ left: anchor(right);
+}
+
+.bottom.right {
+ position-area: bottom right;
+}
+
+.bottom.left {
+ position-area: bottom left;
+}
+
+.top.right {
+ position-area: top right;
+}
+
+.top.left {
+ position-area: top left;
+}
+</style>
+<div id=cb class=abs-cb>
+ <div style="height: 75px;"></div>
+ <div class=anchor></div>
+ <div id="top-right" class="positioned top right"></div>
+ <div id="bottom-right" class="positioned bottom right"></div>
+ <div id="bottom-left" class="positioned bottom left"></div>
+ <div id="top-left" class="positioned top left"></div>
+</div>
+<script>
+const opposite = {
+ 'top': 'bottom',
+ 'bottom': 'top',
+ 'left': 'right',
+ 'right': 'left',
+};
+function test_positioned(y, x) {
+ test(() => {
+ const d = document.getElementById(`${y}-${x}`);
+ const s = getComputedStyle(d);
+ assert_equals(s.getPropertyValue(opposite[y]), '0px');
+ assert_equals(s.getPropertyValue(opposite[x]), '0px');
+ }, `Offsets for ${y} ${x} positioned`);
+}
+
+test_positioned('top', 'right');
+test_positioned('bottom', 'right');
+test_positioned('bottom', 'left');
+test_positioned('top', 'left');
+</script>