Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/web_ui/dev/goldens_lock.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
repository: https://github.com/flutter/goldens.git
revision: bb55871d3803337053f7200b8690a4c1322e82ea
revision: 4b4c256d6124a135b70c1a9a7ff10cf2827df31c
136 changes: 89 additions & 47 deletions lib/web_ui/lib/src/engine/text/layout_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,36 +78,40 @@ class TextLayoutService {
final Spanometer spanometer = Spanometer(paragraph, context);

int spanIndex = 0;
ParagraphSpan span = paragraph.spans[0];
LineBuilder currentLine =
LineBuilder.first(paragraph, spanometer, maxWidth: constraints.width);

// The only way to exit this while loop is by hitting one of the `break;`
// statements (e.g. when we reach `endOfText`, when ellipsis has been
// appended).
while (true) {
// *********************************************** //
// *** HANDLE HARD LINE BREAKS AND END OF TEXT *** //
// *********************************************** //

if (currentLine.end.isHard) {
if (currentLine.isNotEmpty) {
// ************************** //
// *** HANDLE END OF TEXT *** //
// ************************** //

// All spans have been consumed.
final bool reachedEnd = spanIndex == spanCount;
if (reachedEnd) {
// In some cases, we need to extend the line to the end of text and
// build it:
//
// 1. Line is not empty. This could happen when the last span is a
// placeholder.
//
// 2. We haven't reached `LineBreakType.endOfText` yet. This could
// happen when the last character is a new line.
if (currentLine.isNotEmpty || currentLine.end.type != LineBreakType.endOfText) {
currentLine.extendToEndOfText();
lines.add(currentLine.build());
if (currentLine.end.type != LineBreakType.endOfText) {
currentLine = currentLine.nextLine();
}
}

if (currentLine.end.type == LineBreakType.endOfText) {
break;
}
break;
}

// ********************************* //
// *** THE MAIN MEASUREMENT PART *** //
// ********************************* //

final isLastSpan = spanIndex == spanCount - 1;
final ParagraphSpan span = paragraph.spans[spanIndex];

if (span is PlaceholderSpan) {
if (currentLine.widthIncludingSpace + span.width <= constraints.width) {
Expand All @@ -121,11 +125,7 @@ class TextLayoutService {
}
currentLine.addPlaceholder(span);
}

if (isLastSpan) {
lines.add(currentLine.build());
break;
}
spanIndex++;
} else if (span is FlatTextSpan) {
spanometer.currentSpan = span;
final LineBreakResult nextBreak = currentLine.findNextBreak(span.end);
Expand All @@ -138,6 +138,10 @@ class TextLayoutService {

// The line can extend to `nextBreak` without overflowing.
currentLine.extendTo(nextBreak);
if (nextBreak.type == LineBreakType.mandatory) {
lines.add(currentLine.build());
currentLine = currentLine.nextLine();
}
} else {
// The chunk of text can't fit into the current line.
final bool isLastLine =
Expand Down Expand Up @@ -165,23 +169,19 @@ class TextLayoutService {
currentLine = currentLine.nextLine();
}
}

// Only go to the next span if we've reached the end of this span.
if (currentLine.end.index >= span.end) {
currentLine.createBox();
++spanIndex;
}
} else {
throw UnimplementedError('Unknown span type: ${span.runtimeType}');
}

if (lines.length == maxLines) {
break;
}

// ********************************************* //
// *** ADVANCE TO THE NEXT SPAN IF NECESSARY *** //
// ********************************************* //

// Only go to the next span if we've reached the end of this span.
if (currentLine.end.index >= span.end && spanIndex < spanCount - 1) {
currentLine.createBox();
span = paragraph.spans[++spanIndex];
}
}

// ************************************************** //
Expand All @@ -205,20 +205,33 @@ class TextLayoutService {
// ******************************** //

spanIndex = 0;
span = paragraph.spans[0];
currentLine =
LineBuilder.first(paragraph, spanometer, maxWidth: constraints.width);

while (currentLine.end.type != LineBreakType.endOfText) {
while (spanIndex < spanCount) {
final ParagraphSpan span = paragraph.spans[spanIndex];
bool breakToNextLine = false;

if (span is PlaceholderSpan) {
currentLine.addPlaceholder(span);
spanIndex++;
} else if (span is FlatTextSpan) {
spanometer.currentSpan = span;
final LineBreakResult nextBreak = currentLine.findNextBreak(span.end);

// For the purpose of max intrinsic width, we don't care if the line
// fits within the constraints or not. So we always extend it.
currentLine.extendTo(nextBreak);
if (nextBreak.type == LineBreakType.mandatory) {
// We don't want to break the line now because we want to update
// min/max intrinsic widths below first.
breakToNextLine = true;
}

// Only go to the next span if we've reached the end of this span.
if (currentLine.end.index >= span.end) {
spanIndex++;
}
}

final double widthOfLastSegment = currentLine.lastSegment.width;
Expand All @@ -231,19 +244,9 @@ class TextLayoutService {
maxIntrinsicWidth = currentLine.widthIncludingSpace;
}

if (currentLine.end.isHard) {
if (breakToNextLine) {
currentLine = currentLine.nextLine();
}

// Only go to the next span if we've reached the end of this span.
if (currentLine.end.index >= span.end) {
if (spanIndex < spanCount - 1) {
span = paragraph.spans[++spanIndex];
} else {
// We reached the end of the last span in the paragraph.
break;
}
}
}
}

Expand Down Expand Up @@ -761,12 +764,19 @@ class LineBuilder {
return widthOfTrailingSpace + spanometer.measure(end, newEnd);
}

bool get _isLastBoxAPlaceholder {
if (_boxes.isEmpty) {
return false;
}
return (_boxes.last is PlaceholderBox);
}

/// Extends the line by setting a [newEnd].
void extendTo(LineBreakResult newEnd) {
// If the current end of the line is a hard break, the line shouldn't be
// extended any further.
assert(
isEmpty || !end.isHard,
isEmpty || !end.isHard || _isLastBoxAPlaceholder,
'Cannot extend a line that ends with a hard break.',
);

Expand All @@ -776,6 +786,28 @@ class LineBuilder {
_addSegment(_createSegment(newEnd));
}

/// Extends the line to the end of the paragraph.
void extendToEndOfText() {
if (end.type == LineBreakType.endOfText) {
return;
}

final LineBreakResult endOfText = LineBreakResult.sameIndex(
paragraph.toPlainText().length,
LineBreakType.endOfText,
);

// The spanometer may not be ready in some cases. E.g. when the paragraph
// is made up of only placeholders and no text.
if (spanometer.isReady) {
ascent = math.max(ascent, spanometer.ascent);
descent = math.max(descent, spanometer.descent);
_addSegment(_createSegment(endOfText));
} else {
end = endOfText;
}
}

void addPlaceholder(PlaceholderSpan placeholder) {
// Increase the line's height to fit the placeholder, if necessary.
final double ascent, descent;
Expand Down Expand Up @@ -1024,7 +1056,7 @@ class LineBuilder {
final LineBreakResult boxEnd = end;
// Avoid creating empty boxes. This could happen when the end of a span
// coincides with the end of a line. In this case, `createBox` is called twice.
if (boxStart == boxEnd) {
if (boxStart.index == boxEnd.index) {
return;
}

Expand All @@ -1045,13 +1077,20 @@ class LineBuilder {
final double ellipsisWidth =
ellipsis == null ? 0.0 : spanometer.measureText(ellipsis);

final int endIndexWithoutNewlines = math.max(start.index, end.indexWithoutTrailingNewlines);
final bool hardBreak;
if (end.type != LineBreakType.endOfText && _isLastBoxAPlaceholder) {
hardBreak = false;
} else {
hardBreak = end.isHard;
}
return EngineLineMetrics.rich(
lineNumber,
ellipsis: ellipsis,
startIndex: start.index,
endIndex: end.index,
endIndexWithoutNewlines: end.indexWithoutTrailingNewlines,
hardBreak: end.isHard,
endIndexWithoutNewlines: endIndexWithoutNewlines,
hardBreak: hardBreak,
width: width + ellipsisWidth,
widthWithTrailingSpaces: widthIncludingSpace + ellipsisWidth,
left: alignOffset,
Expand Down Expand Up @@ -1150,6 +1189,9 @@ class Spanometer {
}
}

/// Whether the spanometer is ready to take measurements.
bool get isReady => _currentSpan != null;

/// The distance from the top of the current span to the alphabetic baseline.
double get ascent => _currentRuler!.alphabeticBaseline;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,7 @@ void testMain() async {
canvas.drawParagraph(paragraph, offset);

// Then fill the placeholders.
final TextBox placeholderBox = paragraph.getBoxesForPlaceholders().single;
final SurfacePaint redPaint = Paint()..color = red;
canvas.drawRect(placeholderBox.toRect().shift(offset), redPaint.paintData);
fillPlaceholder(canvas, offset, paragraph);

offset = offset.translate(0.0, paragraph.height + 30.0);
}
Expand Down Expand Up @@ -86,9 +84,7 @@ void testMain() async {
canvas.drawParagraph(paragraph, offset);

// Then fill the placeholders.
final TextBox placeholderBox = paragraph.getBoxesForPlaceholders().single;
final SurfacePaint redPaint = Paint()..color = red;
canvas.drawRect(placeholderBox.toRect().shift(offset), redPaint.paintData);
fillPlaceholder(canvas, offset, paragraph);

offset = offset.translate(0.0, paragraph.height + 30.0);
}
Expand Down Expand Up @@ -122,13 +118,89 @@ void testMain() async {
canvas.drawParagraph(paragraph, offset);

// Then fill the placeholders.
final TextBox placeholderBox = paragraph.getBoxesForPlaceholders().single;
final SurfacePaint redPaint = Paint()..color = red;
canvas.drawRect(placeholderBox.toRect().shift(offset), redPaint.paintData);
fillPlaceholder(canvas, offset, paragraph);

offset = offset.translate(0.0, paragraph.height + 30.0);
}

return takeScreenshot(canvas, bounds, 'canvas_paragraph_placeholders_align_dom');
});

test('draws paragraphs starting or ending with a placeholder', () {
const Rect bounds = Rect.fromLTWH(0, 0, 420, 300);
final canvas = BitmapCanvas(bounds, RenderStrategy());

Offset offset = Offset(10, 10);

// First paragraph with a placeholder at the beginning.
final CanvasParagraph paragraph1 = rich(
ParagraphStyle(fontFamily: 'Roboto', fontSize: 24.0, textAlign: TextAlign.center),
(builder) {
builder.addPlaceholder(80.0, 50.0, PlaceholderAlignment.baseline, baseline: TextBaseline.alphabetic);
builder.pushStyle(TextStyle(color: black));
builder.addText(' Lorem ipsum.');
},
)..layout(constrain(400.0));

// Draw the paragraph.
canvas.drawParagraph(paragraph1, offset);
fillPlaceholder(canvas, offset, paragraph1);
surroundParagraph(canvas, offset, paragraph1);

offset = offset.translate(0.0, paragraph1.height + 30.0);

// Second paragraph with a placeholder at the end.
final CanvasParagraph paragraph2 = rich(
ParagraphStyle(fontFamily: 'Roboto', fontSize: 24.0, textAlign: TextAlign.center),
(builder) {
builder.pushStyle(TextStyle(color: black));
builder.addText('Lorem ipsum ');
builder.addPlaceholder(80.0, 50.0, PlaceholderAlignment.baseline, baseline: TextBaseline.alphabetic);
},
)..layout(constrain(400.0));

// Draw the paragraph.
canvas.drawParagraph(paragraph2, offset);
fillPlaceholder(canvas, offset, paragraph2);
surroundParagraph(canvas, offset, paragraph2);

offset = offset.translate(0.0, paragraph2.height + 30.0);

// Third paragraph with a placeholder alone in the second line.
final CanvasParagraph paragraph3 = rich(
ParagraphStyle(fontFamily: 'Roboto', fontSize: 24.0, textAlign: TextAlign.center),
(builder) {
builder.pushStyle(TextStyle(color: black));
builder.addText('Lorem ipsum ');
builder.addPlaceholder(80.0, 50.0, PlaceholderAlignment.baseline, baseline: TextBaseline.alphabetic);
},
)..layout(constrain(200.0));

// Draw the paragraph.
canvas.drawParagraph(paragraph3, offset);
fillPlaceholder(canvas, offset, paragraph3);
surroundParagraph(canvas, offset, paragraph3);

return takeScreenshot(canvas, bounds, 'canvas_paragraph_placeholders_start_and_end');
});
}

void surroundParagraph(
EngineCanvas canvas,
Offset offset,
CanvasParagraph paragraph,
) {
final Rect rect = offset & Size(paragraph.width, paragraph.height);
final SurfacePaint paint = Paint()..color = blue..style = PaintingStyle.stroke;
canvas.drawRect(rect, paint.paintData);
}

void fillPlaceholder(
EngineCanvas canvas,
Offset offset,
CanvasParagraph paragraph,
) {
final TextBox placeholderBox = paragraph.getBoxesForPlaceholders().single;
final SurfacePaint paint = Paint()..color = red;
canvas.drawRect(placeholderBox.toRect().shift(offset), paint.paintData);
}
Loading