layout method
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();
}
}
}