Skip to content

Commit 8547218

Browse files
authored
[web] Implement TextAlign.justify (flutter#29158)
1 parent b83eabc commit 8547218

File tree

7 files changed

+299
-73
lines changed

7 files changed

+299
-73
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: 9c36f57f1a673a7ab444f4f20df16601dde15335
2+
revision: 8e169f3ca5cf4af283a2ff4cfd98a564fb173adf

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ class CanvasParagraph implements EngineParagraph {
185185
}
186186

187187
final EngineLineMetrics line = lines[i];
188-
final List<RangeBox> boxes = line.boxes!;
188+
final List<RangeBox> boxes = line.boxes;
189189
final StringBuffer buffer = StringBuffer();
190190

191191
int j = 0;

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ class TextLayoutService {
273273
List<ui.TextBox> getBoxesForPlaceholders() {
274274
final List<ui.TextBox> boxes = <ui.TextBox>[];
275275
for (final EngineLineMetrics line in lines) {
276-
for (final RangeBox box in line.boxes!) {
276+
for (final RangeBox box in line.boxes) {
277277
if (box is PlaceholderBox) {
278278
boxes.add(box.toTextBox(line));
279279
}
@@ -303,7 +303,7 @@ class TextLayoutService {
303303

304304
for (final EngineLineMetrics line in lines) {
305305
if (line.overlapsWith(start, end)) {
306-
for (final RangeBox box in line.boxes!) {
306+
for (final RangeBox box in line.boxes) {
307307
if (box is SpanBox && box.overlapsWith(start, end)) {
308308
boxes.add(box.intersect(line, start, end));
309309
}
@@ -336,7 +336,7 @@ class TextLayoutService {
336336
}
337337

338338
final double dx = offset.dx - line.left;
339-
for (final RangeBox box in line.boxes!) {
339+
for (final RangeBox box in line.boxes) {
340340
if (box.left <= dx && dx <= box.right) {
341341
return box.getPositionForX(dx);
342342
}
@@ -892,6 +892,8 @@ class LineBuilder {
892892
/// Whether the end of this line is a prohibited break.
893893
bool get isEndProhibited => end.type == LineBreakType.prohibited;
894894

895+
int _spaceBoxCount = 0;
896+
895897
bool get isEmpty => _segments.isEmpty;
896898
bool get isNotEmpty => _segments.isNotEmpty;
897899

@@ -1131,6 +1133,9 @@ class LineBuilder {
11311133
if (_currentBoxStart.index > poppedSegment.start.index) {
11321134
final RangeBox poppedBox = _boxes.removeLast();
11331135
_currentBoxStartOffset -= poppedBox.width;
1136+
if (poppedBox is SpanBox && poppedBox.isSpaceOnly) {
1137+
_spaceBoxCount--;
1138+
}
11341139
}
11351140

11361141
return poppedSegment;
@@ -1274,6 +1279,10 @@ class LineBuilder {
12741279
isSpaceOnly: isSpaceOnly,
12751280
));
12761281

1282+
if (isSpaceOnly) {
1283+
_spaceBoxCount++;
1284+
}
1285+
12771286
_currentBoxStartOffset = widthIncludingSpace;
12781287
}
12791288

@@ -1308,6 +1317,7 @@ class LineBuilder {
13081317
ascent: ascent,
13091318
descent: descent,
13101319
boxes: _boxes,
1320+
spaceBoxCount: _spaceBoxCount,
13111321
);
13121322
}
13131323

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

Lines changed: 101 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -22,60 +22,105 @@ class TextPaintService {
2222
// individually.
2323
final List<EngineLineMetrics> lines = paragraph.computeLineMetrics();
2424

25+
if (lines.isEmpty) {
26+
return;
27+
}
28+
29+
final EngineLineMetrics lastLine = lines.last;
2530
for (final EngineLineMetrics line in lines) {
26-
for (final RangeBox box in line.boxes!) {
27-
_paintBox(canvas, offset, line, box);
31+
if (line.boxes.isEmpty) {
32+
continue;
33+
}
34+
35+
final RangeBox lastBox = line.boxes.last;
36+
final double justifyPerSpaceBox =
37+
_calculateJustifyPerSpaceBox(paragraph, line, lastLine, lastBox);
38+
39+
ui.Offset justifiedOffset = offset;
40+
41+
for (final RangeBox box in line.boxes) {
42+
final bool isTrailingSpaceBox =
43+
box == lastBox && box is SpanBox && box.isSpaceOnly;
44+
45+
// Don't paint background for the trailing space in the line.
46+
if (!isTrailingSpaceBox) {
47+
_paintBackground(canvas, justifiedOffset, line, box, justifyPerSpaceBox);
48+
}
49+
_paintText(canvas, justifiedOffset, line, box);
50+
51+
if (box is SpanBox && box.isSpaceOnly && justifyPerSpaceBox != 0.0) {
52+
justifiedOffset = justifiedOffset.translate(justifyPerSpaceBox, 0.0);
53+
}
2854
}
2955
}
3056
}
3157

32-
void _paintBox(
58+
void _paintBackground(
3359
BitmapCanvas canvas,
3460
ui.Offset offset,
3561
EngineLineMetrics line,
3662
RangeBox box,
63+
double justifyPerSpaceBox,
3764
) {
38-
// Placeholder spans don't need any painting. Their boxes should remain
39-
// empty so that their underlying widgets do their own painting.
4065
if (box is SpanBox) {
4166
final FlatTextSpan span = box.span;
4267

4368
// Paint the background of the box, if the span has a background.
4469
final SurfacePaint? background = span.style.background as SurfacePaint?;
4570
if (background != null) {
46-
canvas.drawRect(
47-
box.toTextBox(line).toRect().shift(offset),
48-
background.paintData,
49-
);
71+
ui.Rect rect = box.toTextBox(line).toRect().shift(offset);
72+
if (box.isSpaceOnly) {
73+
rect = ui.Rect.fromPoints(
74+
rect.topLeft,
75+
rect.bottomRight.translate(justifyPerSpaceBox, 0.0),
76+
);
77+
}
78+
canvas.drawRect(rect, background.paintData);
5079
}
80+
}
81+
}
82+
83+
void _paintText(
84+
BitmapCanvas canvas,
85+
ui.Offset offset,
86+
EngineLineMetrics line,
87+
RangeBox box,
88+
) {
89+
// There's no text to paint in placeholder spans.
90+
if (box is SpanBox) {
91+
final FlatTextSpan span = box.span;
5192

52-
// Paint the actual text.
5393
_applySpanStyleToCanvas(span, canvas);
5494
final double x = offset.dx + line.left + box.left;
5595
final double y = offset.dy + line.baseline;
56-
final String text = paragraph.toPlainText().substring(
57-
box.start.index,
58-
box.end.indexWithoutTrailingNewlines,
59-
);
60-
final double? letterSpacing = span.style.letterSpacing;
61-
if (letterSpacing == null || letterSpacing == 0.0) {
62-
canvas.fillText(text, x, y, shadows: span.style.shadows);
63-
} else {
64-
// TODO(mdebbar): Implement letter-spacing on canvas more efficiently:
65-
// https://github.com/flutter/flutter/issues/51234
66-
double charX = x;
67-
final int len = text.length;
68-
for (int i = 0; i < len; i++) {
69-
final String char = text[i];
70-
canvas.fillText(char, charX.roundToDouble(), y,
71-
shadows: span.style.shadows);
72-
charX += letterSpacing + canvas.measureText(char).width!;
96+
97+
// Don't paint the text for space-only boxes. This is just an
98+
// optimization, it doesn't have any effect on the output.
99+
if (!box.isSpaceOnly) {
100+
final String text = paragraph.toPlainText().substring(
101+
box.start.index,
102+
box.end.indexWithoutTrailingNewlines,
103+
);
104+
final double? letterSpacing = span.style.letterSpacing;
105+
if (letterSpacing == null || letterSpacing == 0.0) {
106+
canvas.fillText(text, x, y, shadows: span.style.shadows);
107+
} else {
108+
// TODO(mdebbar): Implement letter-spacing on canvas more efficiently:
109+
// https://github.com/flutter/flutter/issues/51234
110+
double charX = x;
111+
final int len = text.length;
112+
for (int i = 0; i < len; i++) {
113+
final String char = text[i];
114+
canvas.fillText(char, charX.roundToDouble(), y,
115+
shadows: span.style.shadows);
116+
charX += letterSpacing + canvas.measureText(char).width!;
117+
}
73118
}
74119
}
75120

76121
// Paint the ellipsis using the same span styles.
77122
final String? ellipsis = line.ellipsis;
78-
if (ellipsis != null && box == line.boxes!.last) {
123+
if (ellipsis != null && box == line.boxes.last) {
79124
final double x = offset.dx + line.left + box.right;
80125
canvas.fillText(ellipsis, x, y);
81126
}
@@ -97,3 +142,31 @@ class TextPaintService {
97142
canvas.setUpPaint(paint.paintData, null);
98143
}
99144
}
145+
146+
/// Calculates for the given [line], the amount of extra width that needs to be
147+
/// added to each space box in order to align the line with the rest of the
148+
/// paragraph.
149+
double _calculateJustifyPerSpaceBox(
150+
CanvasParagraph paragraph,
151+
EngineLineMetrics line,
152+
EngineLineMetrics lastLine,
153+
RangeBox lastBox,
154+
) {
155+
// Don't apply any justification on the last line.
156+
if (line != lastLine &&
157+
paragraph.width.isFinite &&
158+
paragraph.paragraphStyle.textAlign == ui.TextAlign.justify) {
159+
final double justifyTotal = paragraph.width - line.width;
160+
161+
int spaceBoxesToJustify = line.spaceBoxCount;
162+
// If the last box is a space box, we can't use it to justify text.
163+
if (lastBox is SpanBox && lastBox.isSpaceOnly) {
164+
spaceBoxesToJustify--;
165+
}
166+
if (spaceBoxesToJustify > 0) {
167+
return justifyTotal / spaceBoxesToJustify;
168+
}
169+
}
170+
171+
return 0.0;
172+
}

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

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -31,33 +31,8 @@ class EngineLineMetrics implements ui.LineMetrics {
3131
endIndex = -1,
3232
endIndexWithoutNewlines = -1,
3333
widthWithTrailingSpaces = width,
34-
boxes = null;
35-
36-
EngineLineMetrics.withText(
37-
String this.displayText, {
38-
required this.startIndex,
39-
required this.endIndex,
40-
required this.endIndexWithoutNewlines,
41-
required this.hardBreak,
42-
required this.width,
43-
required this.widthWithTrailingSpaces,
44-
required this.left,
45-
required this.lineNumber,
46-
}) : assert(displayText != null), // ignore: unnecessary_null_comparison,
47-
assert(startIndex != null), // ignore: unnecessary_null_comparison
48-
assert(endIndex != null), // ignore: unnecessary_null_comparison
49-
assert(endIndexWithoutNewlines != null), // ignore: unnecessary_null_comparison
50-
assert(hardBreak != null), // ignore: unnecessary_null_comparison
51-
assert(width != null), // ignore: unnecessary_null_comparison
52-
assert(left != null), // ignore: unnecessary_null_comparison
53-
assert(lineNumber != null && lineNumber >= 0), // ignore: unnecessary_null_comparison
54-
ellipsis = null,
55-
ascent = double.infinity,
56-
descent = double.infinity,
57-
unscaledAscent = double.infinity,
58-
height = double.infinity,
59-
baseline = double.infinity,
60-
boxes = null;
34+
boxes = <RangeBox>[],
35+
spaceBoxCount = 0;
6136

6237
EngineLineMetrics.rich(
6338
this.lineNumber, {
@@ -74,6 +49,7 @@ class EngineLineMetrics implements ui.LineMetrics {
7449
required this.ascent,
7550
required this.descent,
7651
required this.boxes,
52+
required this.spaceBoxCount,
7753
}) : displayText = null,
7854
unscaledAscent = double.infinity;
7955

@@ -101,7 +77,10 @@ class EngineLineMetrics implements ui.LineMetrics {
10177

10278
/// The list of boxes representing the entire line, possibly across multiple
10379
/// spans.
104-
final List<RangeBox>? boxes;
80+
final List<RangeBox> boxes;
81+
82+
/// The number of boxes that are space-only.
83+
final int spaceBoxCount;
10584

10685
@override
10786
final bool hardBreak;

0 commit comments

Comments
 (0)