layout method

Size layout(
  1. BoxConstraints constraints
)

Perform layout with given constraints.

Implementation

Size layout(BoxConstraints constraints) {
  if (!kReleaseMode) {
    developer.Timeline.startSync('InlineFormattingContext.layout');
  }
  try {
    // Prepare items if needed
    prepareLayout();
    // Two-pass build: first lay out without right-extras placeholders to
    // observe natural breaks, then re-layout with right-extras only for
    // inline elements that do not fragment across lines.
    _suppressAllRightExtras = true;
    _forceRightExtrasOwners.clear();
    _buildAndLayoutParagraph(constraints);
    // Compute baseline offsets for text-run vertical-align placeholders (top/middle/bottom)
    bool needsVARebuild = _computeTextRunBaselineOffsets() | _computeAtomicBaselineOffsets();

    // Second pass: Only add right-extras placeholders for inline elements that
    // did NOT fragment across lines in pass 1. For fragmented spans, we rely on
    // per-line trailing reserves to avoid altering the chosen breaks.
    _forceRightExtrasOwners.clear();
    for (final entry in _elementRanges.entries) {
      final box = entry.key;
      final (int sIdx, int eIdx) = entry.value;
      if (eIdx <= sIdx) continue;
      final styleR = box.renderStyle;
      final double extraR = styleR.paddingRight.computedValue +
          styleR.effectiveBorderRightWidth.computedValue +
          styleR.marginRight.computedValue;
      if (extraR <= 0) continue;
      final rects = _paragraph!.getBoxesForRange(sIdx, eIdx);
      if (rects.isEmpty) continue;
      final int firstLine = _lineIndexForRect(rects.first);
      final int lastLine = _lineIndexForRect(rects.last);
      if (firstLine >= 0 && firstLine == lastLine) {
        _forceRightExtrasOwners.add(box);
      }
    }
    if (_forceRightExtrasOwners.isNotEmpty || needsVARebuild) {
      _suppressAllRightExtras = false;
      _buildAndLayoutParagraph(constraints);
      // Clear offsets after they are consumed in PASS 2
      _textRunBaselineOffsets = null;
      _atomicBaselineOffsets = null;
    }

    // Compute size from paragraph
    final para = _paragraph!;
    // For nowrap+ellipsis scenarios (with overflow not visible), browsers keep the
    // line box width equal to the available content width so truncation/ellipsis
    // can occur at the right edge. Honor the bounded width directly in this case.
    final CSSRenderStyle cStyle = (container as RenderBoxModel).renderStyle;
    final bool wantsEllipsis = cStyle.effectiveTextOverflow == TextOverflow.ellipsis &&
        (cStyle.effectiveOverflowX != CSSOverflowType.visible) &&
        (cStyle.whiteSpace == WhiteSpace.nowrap || cStyle.whiteSpace == WhiteSpace.pre);
    // Use visual longest line for general shrink-to-fit; override with bounded width when keeping ellipsis.
    double width = _computeVisualLongestLine();
    // If we reflowed the paragraph to the available width for alignment,
    // report that width as the IFC width so block containers use the full
    // content inline-size (preserving text-align).
    if (_paraReflowedToAvailWidthForAlign &&
        constraints.hasBoundedWidth &&
        constraints.maxWidth.isFinite &&
        constraints.maxWidth > 0) {
      width = constraints.maxWidth;
    }
    // For left/start alignment, if we shaped wide due to unbreakable detection but the
    // natural single-line width fits within the bounded available width, report the
    // bounded width as the IFC width. Keep left-shift in painting for coordinate mapping.
    if (!_paraReflowedToAvailWidthForAlign &&
        _paragraphShapedWithHugeWidth &&
        constraints.hasBoundedWidth &&
        constraints.maxWidth.isFinite &&
        constraints.maxWidth > 0) {
      // Avoid overriding width reporting for out-of-flow positioned containers
      // (absolute/fixed). In those cases, leave the paragraph width based on
      // natural line width to prevent interfering with positioned layout.
      final CSSPositionType posType = (container as RenderBoxModel).renderStyle.position;
      final bool containerIsOutOfFlow = posType == CSSPositionType.absolute || posType == CSSPositionType.fixed;
      if (!containerIsOutOfFlow) {
        final double natural = _paragraph?.longestLine ?? width;
        if (constraints.maxWidth + 0.5 >= natural) {
          width = constraints.maxWidth;
        }
      }
    }
    if (wantsEllipsis && constraints.hasBoundedWidth && constraints.maxWidth.isFinite && constraints.maxWidth > 0) {
      width = constraints.maxWidth;
    }
    double height = para.height;

    if (!_paraReflowedToAvailWidthForAlign && _paragraphShapedWithHugeWidth) {
      if (constraints.hasBoundedWidth && constraints.maxWidth.isFinite && constraints.maxWidth > 0) {
        final CSSRenderStyle cStyle2 = (container as RenderBoxModel).renderStyle;
        final TextAlign ta = cStyle2.textAlign;
        final TextDirection dir = cStyle2.direction;
        final bool isRtl = dir == TextDirection.rtl;
        final bool wantsAlignShift = ta == TextAlign.center ||
            ta == TextAlign.right ||
            ta == TextAlign.end ||
            (ta == TextAlign.start && isRtl);
        if (wantsAlignShift) {
          final double cw = constraints.maxWidth;
          final double lineW = _paragraph?.longestLine ?? width;
          double desiredLeft;
          switch (ta) {
            case TextAlign.center:
              desiredLeft = (cw - lineW) / 2.0;
              break;
            case TextAlign.right:
              desiredLeft = cw - lineW;
              break;
            case TextAlign.end:
              desiredLeft = isRtl ? 0.0 : (cw - lineW);
              break;
            case TextAlign.start:
              desiredLeft = isRtl ? (cw - lineW) : 0.0;
              break;
            default:
              desiredLeft = 0.0;
              break;
          }
          double minLeft = 0.0;
          if (_paraLines.isNotEmpty) {
            double m = double.infinity;
            for (final lm in _paraLines) {
              if (lm.left.isFinite) m = math.min(m, lm.left);
            }
            if (m.isFinite) minLeft = m;
          }
          final double forced = minLeft - desiredLeft;
          _paragraphMinLeft = forced;
          _forcedParagraphMinLeftAlignShift = forced;
        }
      }
    }
    // If there's no text (only placeholders) and the container explicitly sets
    // line-height: 0, browsers size each line to the tallest atomic inline on
    // that line without adding extra leading. Flutter's paragraph may report a
    // slightly larger line height due to internal metrics. Normalize by
    // summing per-line max placeholder heights.
    if (_placeholderBoxes.isNotEmpty) {
      // Determine if the paragraph has any real text glyphs (exclude placeholders).
      final int _placeholderCount = math.min(_placeholderBoxes.length, _allPlaceholders.length);
      final bool _hasTextGlyphs = (_paraCharCount - _placeholderCount) > 0;
      // When there is no in-flow text (only atomic inline boxes like inline-block/replaced),
      // browsers size each line to the tallest atomic inline on that line. Vertical margins
      // do not contribute to the line box height. Flutter's paragraph may include margins or
      // extra leading in placeholder rectangles; normalize by summing the per-line maximum
      // owner border-box heights and using that as the paragraph height.
      if (!_hasTextGlyphs) {
        // Build per-line maxes for two measures:
        // - owner border-box height (ignores vertical margins)
        // - placeholder (paragraph) height (includes vertical margins if any)
        final Map<int, double> lineMaxOwner = <int, double>{};
        final Map<int, double> lineMaxTB = <int, double>{};
        final int n = math.min(_placeholderBoxes.length, _allPlaceholders.length);
        for (int i = 0; i < n; i++) {
          final ph = _allPlaceholders[i];
          if (ph.kind != _PHKind.atomic) continue;
          final tb = _placeholderBoxes[i];
          final double tbH = tb.bottom - tb.top;
          final int li = _lineIndexForRect(tb);
          if (li < 0) continue;
          final RenderBox? rb = ph.atomic;
          final RenderBoxModel? styleBox = _resolveStyleBoxForPlaceholder(rb);
          double ownerBorderHeight = 0.0;
          if (styleBox != null) {
            final Size sz = styleBox.boxSize ?? (styleBox.hasSize ? styleBox.size : Size.zero);
            ownerBorderHeight = sz.height.isFinite ? sz.height : 0.0;
          }
          final double prevOwner = lineMaxOwner[li] ?? 0.0;
          if (ownerBorderHeight > prevOwner) lineMaxOwner[li] = ownerBorderHeight;
          final double prevTB = lineMaxTB[li] ?? 0.0;
          if (tbH > prevTB) lineMaxTB[li] = tbH;
        }
        double sumOwner = 0.0;
        if (lineMaxOwner.isNotEmpty) {
          final keys = lineMaxOwner.keys.toList()..sort();
          for (final k in keys) {
            sumOwner += lineMaxOwner[k] ?? 0.0;
          }
        }
        double sumTB = 0.0;
        if (lineMaxTB.isNotEmpty) {
          final keys = lineMaxTB.keys.toList()..sort();
          for (final k in keys) {
            sumTB += lineMaxTB[k] ?? 0.0;
          }
        }
        // Apply spec-driven behavior:
        // - If line-height<=0 explicitly, use placeholder heights (includes inline-block vertical margins)
        //   so lines reflect atomic inline margin-box height as browsers do in this case.
        // - Otherwise, only adjust upward using owner border-box sums when paragraph underestimates.
        final CSSRenderStyle cStyle = (container as RenderBoxModel).renderStyle;
        final CSSLengthValue lh = cStyle.lineHeight;
        if (lh.type != CSSLengthType.NORMAL && lh.computedValue <= 0) {
          if (sumTB > 0.0) {
            height = sumTB;
          }
        } else {
          if (sumOwner > 0.0 && (sumOwner - height) > 0.5) {
            height = sumOwner;
          }
        }
      }
    }
    // Special-case: if the IFC contains only hard line breaks (e.g., one or more
    // <br> elements) and no text or atomic inline content, CSS expects the block
    // to contribute one line box per <br>. Flutter's Paragraph reports an extra
    // trailing empty line in this situation (n breaks -> n+1 lines). Compensate
    // by subtracting the last line height so that a single <br> yields one line
    // instead of two.
    if (_paraLines.isNotEmpty) {
      bool onlyHardBreaks = true;
      for (final it in _items) {
        if (it.type == InlineItemType.text || it.type == InlineItemType.atomicInline) {
          onlyHardBreaks = false;
          break;
        }
      }
      if (onlyHardBreaks) {
        int breakCount = 0;
        for (int i = 0; i < _textContent.length; i++) {
          if (_textContent.codeUnitAt(i) == 0x0A) breakCount++; // '\n'
        }
        if (breakCount > 0) {
          // Paragraph tends to produce breakCount + 1 lines for pure newlines.
          // Subtract the trailing empty line height to match CSS behavior.
          final int expectedParaLines = breakCount + 1;
          if (_paraLines.length >= expectedParaLines) {
            final double lastH = _paraLines.isNotEmpty ? _paraLines.last.height : 0.0;
            if (lastH.isFinite && lastH > 0) {
              height = math.max(0.0, height - lastH);
            }
          }
        }
      }
    }
    // If there is no text and no placeholders, an IFC with purely out-of-flow content
    // contributes 0 to the in-flow content height per CSS.
    if (_paraCharCount == 0 && _placeholderBoxes.isEmpty) {
      height = 0;
    }
    // Scrollable height should account for atomic inline overflow beyond paragraph lines.
    // After paragraph is ready, update parentData.offset for atomic inline children so that
    // paint and hit testing can rely on the standard Flutter offset mechanism.
    _applyAtomicInlineParentDataOffsets();
    return Size(width, height);
  } finally {
    if (!kReleaseMode) {
      developer.Timeline.finishSync();
    }
  }
}