applyPositionedChildOffset static method
Implementation
static void applyPositionedChildOffset(
RenderBoxModel parent,
RenderBoxModel child,
) {
RenderLayoutParentData childParentData =
child.parentData as RenderLayoutParentData;
Size size = child.boxSize!;
Size parentSize = parent.boxSize!;
RenderStyle parentRenderStyle = parent.renderStyle;
CSSLengthValue parentBorderLeftWidth =
parentRenderStyle.effectiveBorderLeftWidth;
CSSLengthValue parentBorderRightWidth =
parentRenderStyle.effectiveBorderRightWidth;
CSSLengthValue parentBorderTopWidth =
parentRenderStyle.effectiveBorderTopWidth;
CSSLengthValue parentBorderBottomWidth =
parentRenderStyle.effectiveBorderBottomWidth;
CSSLengthValue parentPaddingLeft = parentRenderStyle.paddingLeft;
CSSLengthValue parentPaddingTop = parentRenderStyle.paddingTop;
// The containing block of not an inline box is formed by the padding edge of the ancestor.
// Thus the final offset of child need to add the border of parent.
// https://www.w3.org/TR/css-position-3/#def-cb
Size containingBlockSize = Size(
parentSize.width -
parentBorderLeftWidth.computedValue -
parentBorderRightWidth.computedValue,
parentSize.height -
parentBorderTopWidth.computedValue -
parentBorderBottomWidth.computedValue);
CSSRenderStyle childRenderStyle = child.renderStyle;
CSSLengthValue left = childRenderStyle.left;
CSSLengthValue right = childRenderStyle.right;
CSSLengthValue top = childRenderStyle.top;
CSSLengthValue bottom = childRenderStyle.bottom;
CSSLengthValue marginLeft = childRenderStyle.marginLeft;
CSSLengthValue marginRight = childRenderStyle.marginRight;
CSSLengthValue marginTop = childRenderStyle.marginTop;
CSSLengthValue marginBottom = childRenderStyle.marginBottom;
// Fix side effects by render portal.
if (child is RenderEventListener && child.child is RenderBoxModel) {
child = child.child as RenderBoxModel;
childParentData = child.parentData as RenderLayoutParentData;
}
// The static position of positioned element is its offset when its position property had been static
// which equals to the position of its placeholder renderBox.
// https://www.w3.org/TR/CSS2/visudet.html#static-position
RenderPositionPlaceholder? ph =
child.renderStyle.getSelfPositionPlaceHolder();
final bool excludeScrollOffset =
child.renderStyle.position != CSSPositionType.fixed ||
!child.isFixedToViewport;
Offset staticPositionOffset = _getPlaceholderToParentOffset(ph, parent,
excludeScrollOffset: excludeScrollOffset);
// Ensure static position accuracy for W3C compliance
// W3C requires static position to represent where element would be in normal flow
Offset adjustedStaticPosition = _ensureAccurateStaticPosition(
staticPositionOffset,
child,
parent,
left,
right,
top,
bottom,
parentBorderLeftWidth,
parentBorderRightWidth,
parentBorderTopWidth,
parentBorderBottomWidth,
parentPaddingLeft,
parentPaddingTop);
// Inline static-position correction (horizontal): when the placeholder sits inside
// an IFC container (e.g., text followed by abspos inline), align the static X to the
// inline advance within that container's content box so that `left:auto` follows the
// preceding inline content per CSS static-position rules.
// Only apply when the containing block is not the document root. Root cases are handled
// specially below to preserve expected behavior.
if (!parent.isDocumentRootBox && ph != null) {
// Find the nearest ancestor flow container that establishes an IFC.
RenderObject? a = ph.parent;
RenderFlowLayout? flowParent;
while (a != null) {
if (a is RenderFlowLayout && a.establishIFC) {
flowParent = a;
break;
}
a = (a.parent is RenderObject) ? a.parent : null;
}
if (flowParent != null) {
// Only inline-level hypothetical boxes should use inline advance for static X.
// Use specified display (not effective) to avoid misclassifying inline elements
// that are out-of-flow as block.
final CSSDisplay childDisp = child.renderStyle.display;
final bool childIsBlockLike =
(childDisp == CSSDisplay.block || childDisp == CSSDisplay.flex);
// Base content-left inset inside the IFC container
final double contentLeftInset =
flowParent.renderStyle.effectiveBorderLeftWidth.computedValue +
flowParent.renderStyle.paddingLeft.computedValue;
if (!childIsBlockLike) {
// Use IFC-provided inline advance; when unavailable (e.g., empty inline), keep 0.
double inlineAdvance = flowParent.inlineAdvanceBefore(ph);
if (inlineAdvance == 0.0) {
// Fallback: if placeholder is appended after inline content within this IFC container,
// use the paragraph visual max line width as the preceding inline advance.
final bool hasPrecedingInline =
_hasInlineContentBeforePlaceholder(flowParent, ph);
if (hasPrecedingInline &&
flowParent.inlineFormattingContext != null) {
inlineAdvance = flowParent
.inlineFormattingContext!.paragraphVisualMaxLineWidth;
}
}
// Do not use inline advance when the abspos has percentage width (e.g., width:100%),
// since the horizontal insets equation will use the static position as 'left' and a
// percentage width that fills the containing block; browsers effectively align such
// overlays at the content-left (no inline advance).
final bool widthIsPercentage =
child.renderStyle.width.type == CSSLengthType.PERCENTAGE;
final double effAdvance = widthIsPercentage ? 0.0 : inlineAdvance;
// Compute flow content-left in CB space and add inline advance.
final Offset phToFlow = _getPlaceholderToParentOffset(ph, flowParent,
excludeScrollOffset: true);
final double targetX = staticPositionOffset.dx -
phToFlow.dx +
contentLeftInset +
effAdvance;
adjustedStaticPosition = Offset(targetX, adjustedStaticPosition.dy);
// Vertical: when both top and bottom are auto, align to the IFC container's
// content-top in CB coordinates so the abspos sits at the top of the line box.
if (top.isAuto && bottom.isAuto) {
final double contentTopInset = flowParent
.renderStyle.paddingTop.computedValue +
flowParent.renderStyle.effectiveBorderTopWidth.computedValue;
final double targetY =
staticPositionOffset.dy - phToFlow.dy + contentTopInset;
adjustedStaticPosition = Offset(adjustedStaticPosition.dx, targetY);
}
} else {
// Block-level hypothetical box: anchor to flow content-left in CB space.
if (contentLeftInset != 0.0) {
final Offset phToFlow = _getPlaceholderToParentOffset(
ph, flowParent,
excludeScrollOffset: true);
final double targetX =
staticPositionOffset.dx - phToFlow.dx + contentLeftInset;
adjustedStaticPosition = Offset(targetX, adjustedStaticPosition.dy);
}
// Block-level vertical: place below the inline line box height when top/bottom are auto
// AND there is preceding inline content before the placeholder.
if (top.isAuto && bottom.isAuto) {
// Determine preceding inline by structural scan when width sums are unavailable.
final bool hasPrecedingInline =
_hasInlineContentBeforePlaceholder(flowParent, ph);
if (hasPrecedingInline) {
final InlineFormattingContext? ifc =
flowParent.inlineFormattingContext;
if (ifc != null) {
double paraH;
final lines = ifc.paragraphLineMetrics;
if (lines.isNotEmpty) {
paraH = lines.fold<double>(0.0, (sum, lm) => sum + lm.height);
} else {
paraH = ifc.paragraph?.height ?? 0.0;
}
if (paraH != 0.0 && adjustedStaticPosition.dy.abs() < 0.5) {
adjustedStaticPosition =
adjustedStaticPosition.translate(0, paraH);
}
}
}
}
}
// Vertical static position for top/bottom auto in IFC:
// Anchor to the IFC container's content top so the abspos aligns with
// the line box where the placeholder sits (top of the first line).
if (top.isAuto && bottom.isAuto) {
final double padTop = flowParent.renderStyle.paddingTop.computedValue;
final double borderTop =
flowParent.renderStyle.effectiveBorderTopWidth.computedValue;
final double contentTopInset = padTop + borderTop;
if (contentTopInset != 0.0 && adjustedStaticPosition.dy.abs() < 0.5) {
adjustedStaticPosition =
adjustedStaticPosition.translate(0, contentTopInset);
}
}
}
}
// If the containing block is the document root (<html>) and the placeholder lives
// under the block formatting context of <body>, align the static position vertically
// with the first in-flow block-level child’s collapsed top (ignoring parent collapse).
// This matches browser behavior where the first in-flow child’s top margin effectively
// offsets the visible content from the root. The positioned element’s static position
// should reflect that visual start so the out-of-flow element and the following in-flow
// element align vertically when no insets are specified.
if (parent.isDocumentRootBox && ph != null) {
final RenderObject? phParent = ph.parent;
if (phParent is RenderBoxModel) {
final RenderBoxModel phContainer = phParent;
final RenderStyle cStyle = phContainer.renderStyle;
final bool qualifiesBFC = cStyle.isLayoutBox() &&
cStyle.effectiveDisplay == CSSDisplay.block &&
(cStyle.effectiveOverflowY == CSSOverflowType.visible ||
cStyle.effectiveOverflowY == CSSOverflowType.clip) &&
cStyle.paddingTop.computedValue == 0 &&
cStyle.effectiveBorderTopWidth.computedValue == 0;
// Only adjust when placeholder is the first attached child (no previous in-flow block)
final bool isFirstChild = (ph.parentData is RenderLayoutParentData) &&
((ph.parentData as RenderLayoutParentData).previousSibling == null);
if (qualifiesBFC && isFirstChild) {
final RenderBoxModel? firstFlow = _resolveNextInFlowSiblingModel(ph);
if (firstFlow != null) {
final double childTopIgnoringParent =
firstFlow.renderStyle.collapsedMarginTopIgnoringParent;
if (childTopIgnoringParent != 0) {
adjustedStaticPosition =
adjustedStaticPosition.translate(0, childTopIgnoringParent);
}
}
}
// Horizontal static-position in IFC under document root: when placeholder lives in an
// inline formatting context (e.g., <div><span>text</span><abspos/></div>) but the containing
// block is <html>, the static X should reflect the inline advance within the IFC container.
// Compute inline advance from the IFC paragraph; if the placeholder is the last child,
// fall back to the paragraph’s visual max line width.
// Use the nearest IFC container up the chain for horizontal inline advance.
RenderFlowLayout? flowParent;
if (phParent is RenderFlowLayout && phParent.establishIFC) {
flowParent = phParent;
} else {
RenderObject? a = phParent.parent;
while (a != null) {
if (a is RenderFlowLayout && a.establishIFC) {
flowParent = a;
break;
}
a = (a.parent is RenderObject) ? a.parent : null;
}
}
if (flowParent != null) {
// Base inset: content-left inside the IFC container
final double contentLeftInset =
flowParent.renderStyle.effectiveBorderLeftWidth.computedValue +
flowParent.renderStyle.paddingLeft.computedValue;
// Under document root, honor block-level vs inline-level behavior:
// - Block/flex: anchor to content-left only (x = content-left), no vertical shift.
// - Inline-level: add horizontal inline advance before placeholder.
// Use the specified display for block-vs-inline determination; effectiveDisplay
// is normalized for out-of-flow and may not reflect original block-vs-inline.
final CSSDisplay childDispSpecified = child.renderStyle.display;
final bool childIsBlockLike =
(childDispSpecified == CSSDisplay.block ||
childDispSpecified == CSSDisplay.flex);
if (childIsBlockLike) {
if (contentLeftInset != 0.0) {
final Offset phToFlow = _getPlaceholderToParentOffset(
ph, flowParent,
excludeScrollOffset: true);
final double targetX =
staticPositionOffset.dx - phToFlow.dx + contentLeftInset;
adjustedStaticPosition =
Offset(targetX, adjustedStaticPosition.dy);
}
// Vertical: if preceded by inline, move to the next line (add paragraph height).
if (top.isAuto && bottom.isAuto) {
final bool hasPrecedingInline =
_hasInlineContentBeforePlaceholder(flowParent, ph);
if (hasPrecedingInline) {
final InlineFormattingContext? ifc =
flowParent.inlineFormattingContext;
if (ifc != null) {
double paraH;
final lines = ifc.paragraphLineMetrics;
if (lines.isNotEmpty) {
paraH =
lines.fold<double>(0.0, (sum, lm) => sum + lm.height);
} else {
paraH = ifc.paragraph?.height ?? 0.0;
}
if (paraH != 0.0) {
adjustedStaticPosition =
adjustedStaticPosition.translate(0, paraH);
}
}
}
}
} else {
double inlineAdvance = flowParent.inlineAdvanceBefore(ph);
if (inlineAdvance == 0.0) {
final bool hasPrecedingInline =
_hasInlineContentBeforePlaceholder(flowParent, ph);
if (hasPrecedingInline &&
flowParent.inlineFormattingContext != null) {
inlineAdvance = flowParent
.inlineFormattingContext!.paragraphVisualMaxLineWidth;
}
}
final bool widthIsPercentage =
child.renderStyle.width.type == CSSLengthType.PERCENTAGE;
final double effAdvance = widthIsPercentage ? 0.0 : inlineAdvance;
final Offset phToFlow = _getPlaceholderToParentOffset(
ph, flowParent,
excludeScrollOffset: true);
final double targetX = staticPositionOffset.dx -
phToFlow.dx +
contentLeftInset +
effAdvance;
adjustedStaticPosition = Offset(targetX, adjustedStaticPosition.dy);
}
}
}
}
// Child renderObject is reparented under its containing block at build time,
// and staticPositionOffset is already measured relative to the containing block.
// No additional ancestor offset adjustment is needed.
Offset ancestorOffset = Offset.zero;
// ScrollTop and scrollLeft will be added to offset of renderBox in the paint stage
// for positioned fixed element.
if (childRenderStyle.position == CSSPositionType.fixed) {
Offset scrollOffset = Offset.zero;
if (child.isFixedToViewport) {
// Viewport-fixed: compensate all ancestor scroll offsets so the element
// stays anchored to the viewport.
scrollOffset = child.getTotalScrollOffset();
} else {
// Non-viewport fixed (e.g. fixed-position containing block via `transform`):
// compensate only the containing block's own scrolling so the element
// stays anchored within that containing block.
final cbElement =
child.renderStyle.target.holderAttachedContainingBlockElement;
final cbRenderer = cbElement?.attachedRenderer;
if (cbRenderer is RenderBoxModel) {
scrollOffset = Offset(cbRenderer.scrollLeft, cbRenderer.scrollTop);
}
}
child.additionalPaintOffsetX = scrollOffset.dx;
child.additionalPaintOffsetY = scrollOffset.dy;
}
// Determine direction for resolving 'auto' horizontal insets: use the
// direction of the element establishing the static-position containing block
// (typically the IFC container hosting the placeholder) when available;
// otherwise fall back to the containing block's direction.
TextDirection staticContainingDir = parent.renderStyle.direction;
if (ph != null && ph.parent is RenderFlowLayout) {
final RenderFlowLayout flowParent = ph.parent as RenderFlowLayout;
staticContainingDir = flowParent.renderStyle.direction;
}
// For sticky positioning, the insets act as constraints during scroll, not as
// absolute offsets at layout time. Compute the base (un-stuck) offset from the
// static position by treating both axis insets as auto for layout.
final bool isSticky = childRenderStyle.position == CSSPositionType.sticky;
double x = _computePositionedOffset(
Axis.horizontal,
staticContainingDir,
false,
parentBorderLeftWidth,
parentPaddingLeft,
containingBlockSize.width,
size.width,
adjustedStaticPosition.dx,
isSticky ? CSSLengthValue.auto : left,
isSticky ? CSSLengthValue.auto : right,
marginLeft,
marginRight,
);
double y = _computePositionedOffset(
Axis.vertical,
parent.renderStyle.direction,
false,
parentBorderTopWidth,
parentPaddingTop,
containingBlockSize.height,
size.height,
adjustedStaticPosition.dy,
isSticky ? CSSLengthValue.auto : top,
isSticky ? CSSLengthValue.auto : bottom,
marginTop,
marginBottom,
);
final Offset finalOffset = Offset(x, y) - ancestorOffset;
// If this positioned element is wrapped (e.g., by RenderEventListener), ensure
// the wrapper is placed at the positioned offset so its background/border align
// with the child content. The child uses internal offsets relative to the wrapper.
bool placedWrapper = false;
final RenderObject? directParent = child.parent;
if (directParent is RenderEventListener) {
final RenderLayoutParentData pd =
directParent.parentData as RenderLayoutParentData;
pd.offset = finalOffset;
placedWrapper = true;
}
if (!placedWrapper) {
childParentData.offset = finalOffset;
}
}