Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit c1ada67

Browse files
authored
[web] Reland: Fix painting of last placeholder in paragraph (#24905)
1 parent 9d1888e commit c1ada67

File tree

4 files changed

+214
-58
lines changed

4 files changed

+214
-58
lines changed

lib/web_ui/dev/goldens_lock.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
repository: https://github.com/flutter/goldens.git
2-
revision: bb55871d3803337053f7200b8690a4c1322e82ea
2+
revision: 4b4c256d6124a135b70c1a9a7ff10cf2827df31c

lib/web_ui/lib/src/engine/text/layout_service.dart

Lines changed: 89 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -78,36 +78,40 @@ class TextLayoutService {
7878
final Spanometer spanometer = Spanometer(paragraph, context);
7979

8080
int spanIndex = 0;
81-
ParagraphSpan span = paragraph.spans[0];
8281
LineBuilder currentLine =
8382
LineBuilder.first(paragraph, spanometer, maxWidth: constraints.width);
8483

8584
// The only way to exit this while loop is by hitting one of the `break;`
8685
// statements (e.g. when we reach `endOfText`, when ellipsis has been
8786
// appended).
8887
while (true) {
89-
// *********************************************** //
90-
// *** HANDLE HARD LINE BREAKS AND END OF TEXT *** //
91-
// *********************************************** //
92-
93-
if (currentLine.end.isHard) {
94-
if (currentLine.isNotEmpty) {
88+
// ************************** //
89+
// *** HANDLE END OF TEXT *** //
90+
// ************************** //
91+
92+
// All spans have been consumed.
93+
final bool reachedEnd = spanIndex == spanCount;
94+
if (reachedEnd) {
95+
// In some cases, we need to extend the line to the end of text and
96+
// build it:
97+
//
98+
// 1. Line is not empty. This could happen when the last span is a
99+
// placeholder.
100+
//
101+
// 2. We haven't reached `LineBreakType.endOfText` yet. This could
102+
// happen when the last character is a new line.
103+
if (currentLine.isNotEmpty || currentLine.end.type != LineBreakType.endOfText) {
104+
currentLine.extendToEndOfText();
95105
lines.add(currentLine.build());
96-
if (currentLine.end.type != LineBreakType.endOfText) {
97-
currentLine = currentLine.nextLine();
98-
}
99-
}
100-
101-
if (currentLine.end.type == LineBreakType.endOfText) {
102-
break;
103106
}
107+
break;
104108
}
105109

106110
// ********************************* //
107111
// *** THE MAIN MEASUREMENT PART *** //
108112
// ********************************* //
109113

110-
final isLastSpan = spanIndex == spanCount - 1;
114+
final ParagraphSpan span = paragraph.spans[spanIndex];
111115

112116
if (span is PlaceholderSpan) {
113117
if (currentLine.widthIncludingSpace + span.width <= constraints.width) {
@@ -121,11 +125,7 @@ class TextLayoutService {
121125
}
122126
currentLine.addPlaceholder(span);
123127
}
124-
125-
if (isLastSpan) {
126-
lines.add(currentLine.build());
127-
break;
128-
}
128+
spanIndex++;
129129
} else if (span is FlatTextSpan) {
130130
spanometer.currentSpan = span;
131131
final LineBreakResult nextBreak = currentLine.findNextBreak(span.end);
@@ -138,6 +138,10 @@ class TextLayoutService {
138138

139139
// The line can extend to `nextBreak` without overflowing.
140140
currentLine.extendTo(nextBreak);
141+
if (nextBreak.type == LineBreakType.mandatory) {
142+
lines.add(currentLine.build());
143+
currentLine = currentLine.nextLine();
144+
}
141145
} else {
142146
// The chunk of text can't fit into the current line.
143147
final bool isLastLine =
@@ -165,23 +169,19 @@ class TextLayoutService {
165169
currentLine = currentLine.nextLine();
166170
}
167171
}
172+
173+
// Only go to the next span if we've reached the end of this span.
174+
if (currentLine.end.index >= span.end) {
175+
currentLine.createBox();
176+
++spanIndex;
177+
}
168178
} else {
169179
throw UnimplementedError('Unknown span type: ${span.runtimeType}');
170180
}
171181

172182
if (lines.length == maxLines) {
173183
break;
174184
}
175-
176-
// ********************************************* //
177-
// *** ADVANCE TO THE NEXT SPAN IF NECESSARY *** //
178-
// ********************************************* //
179-
180-
// Only go to the next span if we've reached the end of this span.
181-
if (currentLine.end.index >= span.end && spanIndex < spanCount - 1) {
182-
currentLine.createBox();
183-
span = paragraph.spans[++spanIndex];
184-
}
185185
}
186186

187187
// ************************************************** //
@@ -205,20 +205,33 @@ class TextLayoutService {
205205
// ******************************** //
206206

207207
spanIndex = 0;
208-
span = paragraph.spans[0];
209208
currentLine =
210209
LineBuilder.first(paragraph, spanometer, maxWidth: constraints.width);
211210

212-
while (currentLine.end.type != LineBreakType.endOfText) {
211+
while (spanIndex < spanCount) {
212+
final ParagraphSpan span = paragraph.spans[spanIndex];
213+
bool breakToNextLine = false;
214+
213215
if (span is PlaceholderSpan) {
214216
currentLine.addPlaceholder(span);
217+
spanIndex++;
215218
} else if (span is FlatTextSpan) {
216219
spanometer.currentSpan = span;
217220
final LineBreakResult nextBreak = currentLine.findNextBreak(span.end);
218221

219222
// For the purpose of max intrinsic width, we don't care if the line
220223
// fits within the constraints or not. So we always extend it.
221224
currentLine.extendTo(nextBreak);
225+
if (nextBreak.type == LineBreakType.mandatory) {
226+
// We don't want to break the line now because we want to update
227+
// min/max intrinsic widths below first.
228+
breakToNextLine = true;
229+
}
230+
231+
// Only go to the next span if we've reached the end of this span.
232+
if (currentLine.end.index >= span.end) {
233+
spanIndex++;
234+
}
222235
}
223236

224237
final double widthOfLastSegment = currentLine.lastSegment.width;
@@ -231,19 +244,9 @@ class TextLayoutService {
231244
maxIntrinsicWidth = currentLine.widthIncludingSpace;
232245
}
233246

234-
if (currentLine.end.isHard) {
247+
if (breakToNextLine) {
235248
currentLine = currentLine.nextLine();
236249
}
237-
238-
// Only go to the next span if we've reached the end of this span.
239-
if (currentLine.end.index >= span.end) {
240-
if (spanIndex < spanCount - 1) {
241-
span = paragraph.spans[++spanIndex];
242-
} else {
243-
// We reached the end of the last span in the paragraph.
244-
break;
245-
}
246-
}
247250
}
248251
}
249252

@@ -761,12 +764,19 @@ class LineBuilder {
761764
return widthOfTrailingSpace + spanometer.measure(end, newEnd);
762765
}
763766

767+
bool get _isLastBoxAPlaceholder {
768+
if (_boxes.isEmpty) {
769+
return false;
770+
}
771+
return (_boxes.last is PlaceholderBox);
772+
}
773+
764774
/// Extends the line by setting a [newEnd].
765775
void extendTo(LineBreakResult newEnd) {
766776
// If the current end of the line is a hard break, the line shouldn't be
767777
// extended any further.
768778
assert(
769-
isEmpty || !end.isHard,
779+
isEmpty || !end.isHard || _isLastBoxAPlaceholder,
770780
'Cannot extend a line that ends with a hard break.',
771781
);
772782

@@ -776,6 +786,28 @@ class LineBuilder {
776786
_addSegment(_createSegment(newEnd));
777787
}
778788

789+
/// Extends the line to the end of the paragraph.
790+
void extendToEndOfText() {
791+
if (end.type == LineBreakType.endOfText) {
792+
return;
793+
}
794+
795+
final LineBreakResult endOfText = LineBreakResult.sameIndex(
796+
paragraph.toPlainText().length,
797+
LineBreakType.endOfText,
798+
);
799+
800+
// The spanometer may not be ready in some cases. E.g. when the paragraph
801+
// is made up of only placeholders and no text.
802+
if (spanometer.isReady) {
803+
ascent = math.max(ascent, spanometer.ascent);
804+
descent = math.max(descent, spanometer.descent);
805+
_addSegment(_createSegment(endOfText));
806+
} else {
807+
end = endOfText;
808+
}
809+
}
810+
779811
void addPlaceholder(PlaceholderSpan placeholder) {
780812
// Increase the line's height to fit the placeholder, if necessary.
781813
final double ascent, descent;
@@ -1024,7 +1056,7 @@ class LineBuilder {
10241056
final LineBreakResult boxEnd = end;
10251057
// Avoid creating empty boxes. This could happen when the end of a span
10261058
// coincides with the end of a line. In this case, `createBox` is called twice.
1027-
if (boxStart == boxEnd) {
1059+
if (boxStart.index == boxEnd.index) {
10281060
return;
10291061
}
10301062

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

1080+
final int endIndexWithoutNewlines = math.max(start.index, end.indexWithoutTrailingNewlines);
1081+
final bool hardBreak;
1082+
if (end.type != LineBreakType.endOfText && _isLastBoxAPlaceholder) {
1083+
hardBreak = false;
1084+
} else {
1085+
hardBreak = end.isHard;
1086+
}
10481087
return EngineLineMetrics.rich(
10491088
lineNumber,
10501089
ellipsis: ellipsis,
10511090
startIndex: start.index,
10521091
endIndex: end.index,
1053-
endIndexWithoutNewlines: end.indexWithoutTrailingNewlines,
1054-
hardBreak: end.isHard,
1092+
endIndexWithoutNewlines: endIndexWithoutNewlines,
1093+
hardBreak: hardBreak,
10551094
width: width + ellipsisWidth,
10561095
widthWithTrailingSpaces: widthIncludingSpace + ellipsisWidth,
10571096
left: alignOffset,
@@ -1150,6 +1189,9 @@ class Spanometer {
11501189
}
11511190
}
11521191

1192+
/// Whether the spanometer is ready to take measurements.
1193+
bool get isReady => _currentSpan != null;
1194+
11531195
/// The distance from the top of the current span to the alphabetic baseline.
11541196
double get ascent => _currentRuler!.alphabeticBaseline;
11551197

lib/web_ui/test/golden_tests/engine/canvas_paragraph/placeholders_test.dart

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,7 @@ void testMain() async {
5050
canvas.drawParagraph(paragraph, offset);
5151

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

5755
offset = offset.translate(0.0, paragraph.height + 30.0);
5856
}
@@ -86,9 +84,7 @@ void testMain() async {
8684
canvas.drawParagraph(paragraph, offset);
8785

8886
// Then fill the placeholders.
89-
final TextBox placeholderBox = paragraph.getBoxesForPlaceholders().single;
90-
final SurfacePaint redPaint = Paint()..color = red;
91-
canvas.drawRect(placeholderBox.toRect().shift(offset), redPaint.paintData);
87+
fillPlaceholder(canvas, offset, paragraph);
9288

9389
offset = offset.translate(0.0, paragraph.height + 30.0);
9490
}
@@ -122,13 +118,89 @@ void testMain() async {
122118
canvas.drawParagraph(paragraph, offset);
123119

124120
// Then fill the placeholders.
125-
final TextBox placeholderBox = paragraph.getBoxesForPlaceholders().single;
126-
final SurfacePaint redPaint = Paint()..color = red;
127-
canvas.drawRect(placeholderBox.toRect().shift(offset), redPaint.paintData);
121+
fillPlaceholder(canvas, offset, paragraph);
128122

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

132126
return takeScreenshot(canvas, bounds, 'canvas_paragraph_placeholders_align_dom');
133127
});
128+
129+
test('draws paragraphs starting or ending with a placeholder', () {
130+
const Rect bounds = Rect.fromLTWH(0, 0, 420, 300);
131+
final canvas = BitmapCanvas(bounds, RenderStrategy());
132+
133+
Offset offset = Offset(10, 10);
134+
135+
// First paragraph with a placeholder at the beginning.
136+
final CanvasParagraph paragraph1 = rich(
137+
ParagraphStyle(fontFamily: 'Roboto', fontSize: 24.0, textAlign: TextAlign.center),
138+
(builder) {
139+
builder.addPlaceholder(80.0, 50.0, PlaceholderAlignment.baseline, baseline: TextBaseline.alphabetic);
140+
builder.pushStyle(TextStyle(color: black));
141+
builder.addText(' Lorem ipsum.');
142+
},
143+
)..layout(constrain(400.0));
144+
145+
// Draw the paragraph.
146+
canvas.drawParagraph(paragraph1, offset);
147+
fillPlaceholder(canvas, offset, paragraph1);
148+
surroundParagraph(canvas, offset, paragraph1);
149+
150+
offset = offset.translate(0.0, paragraph1.height + 30.0);
151+
152+
// Second paragraph with a placeholder at the end.
153+
final CanvasParagraph paragraph2 = rich(
154+
ParagraphStyle(fontFamily: 'Roboto', fontSize: 24.0, textAlign: TextAlign.center),
155+
(builder) {
156+
builder.pushStyle(TextStyle(color: black));
157+
builder.addText('Lorem ipsum ');
158+
builder.addPlaceholder(80.0, 50.0, PlaceholderAlignment.baseline, baseline: TextBaseline.alphabetic);
159+
},
160+
)..layout(constrain(400.0));
161+
162+
// Draw the paragraph.
163+
canvas.drawParagraph(paragraph2, offset);
164+
fillPlaceholder(canvas, offset, paragraph2);
165+
surroundParagraph(canvas, offset, paragraph2);
166+
167+
offset = offset.translate(0.0, paragraph2.height + 30.0);
168+
169+
// Third paragraph with a placeholder alone in the second line.
170+
final CanvasParagraph paragraph3 = rich(
171+
ParagraphStyle(fontFamily: 'Roboto', fontSize: 24.0, textAlign: TextAlign.center),
172+
(builder) {
173+
builder.pushStyle(TextStyle(color: black));
174+
builder.addText('Lorem ipsum ');
175+
builder.addPlaceholder(80.0, 50.0, PlaceholderAlignment.baseline, baseline: TextBaseline.alphabetic);
176+
},
177+
)..layout(constrain(200.0));
178+
179+
// Draw the paragraph.
180+
canvas.drawParagraph(paragraph3, offset);
181+
fillPlaceholder(canvas, offset, paragraph3);
182+
surroundParagraph(canvas, offset, paragraph3);
183+
184+
return takeScreenshot(canvas, bounds, 'canvas_paragraph_placeholders_start_and_end');
185+
});
186+
}
187+
188+
void surroundParagraph(
189+
EngineCanvas canvas,
190+
Offset offset,
191+
CanvasParagraph paragraph,
192+
) {
193+
final Rect rect = offset & Size(paragraph.width, paragraph.height);
194+
final SurfacePaint paint = Paint()..color = blue..style = PaintingStyle.stroke;
195+
canvas.drawRect(rect, paint.paintData);
196+
}
197+
198+
void fillPlaceholder(
199+
EngineCanvas canvas,
200+
Offset offset,
201+
CanvasParagraph paragraph,
202+
) {
203+
final TextBox placeholderBox = paragraph.getBoxesForPlaceholders().single;
204+
final SurfacePaint paint = Paint()..color = red;
205+
canvas.drawRect(placeholderBox.toRect().shift(offset), paint.paintData);
134206
}

0 commit comments

Comments
 (0)