layoutText function
Lays out spans according to paraStyle and maxWidth.
Steps performed:
- Shape each span with shapeText to get ShapedGlyph objects.
- Wrap glyphs greedily at word boundaries (U+0020) to fit
maxWidth. - Handle hard line-breaks (
\n). - Apply
ParagraphStyle.maxLinesandParagraphStyle.ellipsis. - Apply TextAlign to compute each line's LayoutLine.left offset.
- Compute per-line ascent/descent from font metrics.
Implementation
_LayoutResult layoutText(
List<_TextSpan> spans,
ParagraphStyle paraStyle,
double maxWidth,
) {
// ── Step 1: shape all spans into a flat glyph list ──────────────────────
final List<ShapedGlyph> allGlyphs = [];
for (final span in spans) {
final style = span.style;
final String? spanFont =
style != null && style._fontFamily.isNotEmpty ? style._fontFamily : null;
final String? fontFamily = spanFont ??
(paraStyle._fontFamily?.isNotEmpty == true ? paraStyle._fontFamily : null);
if (fontFamily == null) continue;
// Resolve font weight and style for variant lookup.
final FontWeight fontWeight = (style != null &&
(style._encoded[0] & (1 << 5)) != 0)
? FontWeight.values[style._encoded[5]]
: FontWeight.normal;
final FontStyle fontStyle =
(style != null && (style._encoded[0] & (1 << 6)) != 0)
? FontStyle.values[style._encoded[6]]
: FontStyle.normal;
final fontBytes =
FontLoader.getFont(fontFamily, weight: fontWeight, style: fontStyle);
if (fontBytes == null) continue;
final cacheKey = _fontCacheKey(fontFamily, fontWeight, fontStyle);
final font = _pureDartFontCache.putIfAbsent(
cacheKey, () => TtfFont.load(fontBytes));
final double fontSize = style?._fontSize ?? paraStyle._fontSize ?? 14.0;
final effectiveStyle =
style ?? TextStyle(fontSize: fontSize, fontFamily: fontFamily);
allGlyphs.addAll(
shapeText(span.text, effectiveStyle, font, fontKey: cacheKey));
}
if (allGlyphs.isEmpty) return const _LayoutResult([], false);
// ── Step 2: resolve layout parameters from ParagraphStyle ───────────────
final bool hasTextAlign = (paraStyle._encoded[0] & (1 << 1)) != 0;
final TextAlign textAlign =
hasTextAlign ? TextAlign.values[paraStyle._encoded[1]] : TextAlign.left;
final bool hasMaxLines = (paraStyle._encoded[0] & (1 << 5)) != 0;
final int? maxLines = hasMaxLines ? paraStyle._encoded[5] : null;
final String? ellipsis =
(paraStyle._ellipsis?.isNotEmpty == true) ? paraStyle._ellipsis : null;
// ── Step 3: greedy word-wrap ─────────────────────────────────────────────
// Each entry: (glyphs on that line, isHardBreak)
final List<(List<ShapedGlyph>, bool)> rawLines = [];
List<ShapedGlyph> currentLine = [];
double currentWidth = 0.0;
// Index into currentLine of the last space glyph (potential break point).
int lastBreakIdx = -1;
void _flushLine(List<ShapedGlyph> glyphs, bool hard) {
// Strip trailing spaces.
int end = glyphs.length;
while (end > 0 && glyphs[end - 1].isSpace) {
end--;
}
rawLines.add((glyphs.sublist(0, end), hard));
}
for (final glyph in allGlyphs) {
// Hard break on newline.
if (glyph.isNewline) {
_flushLine(List.of(currentLine), true);
currentLine = [];
currentWidth = 0.0;
lastBreakIdx = -1;
continue;
}
// Would adding this glyph overflow the line?
if (currentLine.isNotEmpty &&
maxWidth.isFinite &&
currentWidth + glyph.advance > maxWidth) {
if (lastBreakIdx >= 0) {
// Break at last space: keep glyphs before the space on this line.
final beforeBreak = currentLine.sublist(0, lastBreakIdx);
final afterBreak = currentLine.sublist(lastBreakIdx + 1);
_flushLine(beforeBreak, false);
currentLine = List.of(afterBreak);
currentWidth = currentLine.fold(0.0, (s, g) => s + g.advance);
lastBreakIdx = -1;
} else {
// No break point found: force a character-boundary break.
_flushLine(List.of(currentLine), false);
currentLine = [];
currentWidth = 0.0;
lastBreakIdx = -1;
}
}
if (glyph.isSpace) lastBreakIdx = currentLine.length;
currentLine.add(glyph);
currentWidth += glyph.advance;
}
if (currentLine.isNotEmpty) _flushLine(List.of(currentLine), true);
// ── Step 4: apply maxLines and ellipsis ──────────────────────────────────
bool didExceed = false;
List<(List<ShapedGlyph>, bool)> visibleLines = rawLines;
if (maxLines != null && rawLines.length > maxLines) {
visibleLines = rawLines.sublist(0, maxLines);
didExceed = true;
}
// Apply ellipsis: trim last line so that text + ellipsis fits in maxWidth.
if (ellipsis != null && didExceed && visibleLines.isNotEmpty) {
final lastEntry = visibleLines.last;
final lastGlyphs = List<ShapedGlyph>.of(lastEntry.$1);
// Measure ellipsis width using the style of the last glyph.
double ellipsisWidth = 0.0;
if (lastGlyphs.isNotEmpty) {
final ref = lastGlyphs.last;
final effectiveStyle = TextStyle(
fontSize: ref.fontSize, fontFamily: ref.font.availableTables.first);
for (final g in shapeText(ellipsis, effectiveStyle, ref.font)) {
ellipsisWidth += g.advance;
}
}
// Remove glyphs from the end until the line + ellipsis fits.
double lineW = lastGlyphs.fold(0.0, (s, g) => s + g.advance);
while (lastGlyphs.isNotEmpty && lineW + ellipsisWidth > maxWidth) {
lineW -= lastGlyphs.last.advance;
lastGlyphs.removeLast();
}
// Append ellipsis glyphs.
if (lastGlyphs.isNotEmpty) {
final ref = lastGlyphs.last;
final effectiveStyle =
TextStyle(fontSize: ref.fontSize, color: ref.color);
lastGlyphs.addAll(shapeText(ellipsis, effectiveStyle, ref.font));
}
visibleLines = [
...visibleLines.sublist(0, visibleLines.length - 1),
(lastGlyphs, true),
];
}
// ── Step 5: build LayoutLine objects with positions ──────────────────────
final List<LayoutLine> lines = [];
double yTop = 0.0; // top of current line
for (int i = 0; i < visibleLines.length; i++) {
final (glyphs, hardBreak) = visibleLines[i];
// Per-line ascent/descent from font metrics.
double ascent = 0.0;
double descent = 0.0;
for (final g in glyphs) {
if (g.font.metrics.unitsPerEm == 0) continue;
final scale = g.fontSize / g.font.metrics.unitsPerEm;
final a = g.font.metrics.ascender * scale;
final d = g.font.metrics.descender.abs() * scale;
if (a > ascent) ascent = a;
if (d > descent) descent = d;
}
// Fallback for empty lines.
if (ascent == 0 && descent == 0) {
final fs = paraStyle._fontSize ?? 14.0;
ascent = fs * 0.8;
descent = fs * 0.2;
}
final double baseline = yTop + ascent;
// Glyph x offsets and total line width.
final xOffsets = <double>[];
double x = 0.0;
for (final g in glyphs) {
xOffsets.add(x);
x += g.advance;
}
final lineWidth = x;
// TextAlign horizontal offset.
double left = 0.0;
if (maxWidth.isFinite) {
switch (textAlign) {
case TextAlign.right:
case TextAlign.end:
left = math.max(0.0, maxWidth - lineWidth);
case TextAlign.center:
left = math.max(0.0, (maxWidth - lineWidth) / 2);
default:
left = 0.0;
}
}
lines.add(LayoutLine(
glyphs: glyphs,
xOffsets: xOffsets,
left: left,
baseline: baseline,
ascent: ascent,
descent: descent,
width: lineWidth,
hardBreak: hardBreak,
));
yTop = baseline + descent;
}
return _LayoutResult(lines, didExceed);
}