applyPositionedChildOffset static method

void applyPositionedChildOffset(
  1. RenderBoxModel parent,
  2. RenderBoxModel child
)

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;
  }
}