Skip to content

Commit 40c3a97

Browse files
authored
[web] Fix alignment issue in rich paragraphs (flutter#23965)
1 parent 6d1c65a commit 40c3a97

File tree

5 files changed

+151
-15
lines changed

5 files changed

+151
-15
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: b85f9093e6bc6d4e7cbb7f97491667c143c4a360
2+
revision: 4c3d34f19045a1df10757e5b3cb6c9ace9a6038c

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

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class CanvasParagraph implements EngineParagraph {
2020
required this.paragraphStyle,
2121
required this.plainText,
2222
required this.placeholderCount,
23+
required this.drawOnCanvas,
2324
});
2425

2526
/// The flat list of spans that make up this paragraph.
@@ -34,14 +35,17 @@ class CanvasParagraph implements EngineParagraph {
3435
/// The number of placeholders in this paragraph.
3536
final int placeholderCount;
3637

38+
@override
39+
final bool drawOnCanvas;
40+
3741
@override
3842
double get width => _layoutService.width;
3943

4044
@override
4145
double get height => _layoutService.height;
4246

4347
@override
44-
double get longestLine => _layoutService.longestLine;
48+
double get longestLine => _layoutService.longestLine?.width ?? 0.0;
4549

4650
@override
4751
double get minIntrinsicWidth => _layoutService.minIntrinsicWidth;
@@ -124,6 +128,14 @@ class CanvasParagraph implements EngineParagraph {
124128
return domElement.clone(true) as html.HtmlElement;
125129
}
126130

131+
double _getParagraphAlignOffset() {
132+
final EngineLineMetrics? longestLine = _layoutService.longestLine;
133+
if (longestLine != null) {
134+
return longestLine.left;
135+
}
136+
return 0.0;
137+
}
138+
127139
html.HtmlElement _createDomElement() {
128140
final html.HtmlElement rootElement =
129141
domRenderer.createElement('p') as html.HtmlElement;
@@ -137,6 +149,11 @@ class CanvasParagraph implements EngineParagraph {
137149
// to insert our own <BR> breaks based on layout results.
138150
..whiteSpace = 'pre';
139151

152+
final double alignOffset = _getParagraphAlignOffset();
153+
if (alignOffset != 0.0) {
154+
cssStyle.marginLeft = '${alignOffset}px';
155+
}
156+
140157
if (paragraphStyle._maxLines != null || paragraphStyle._ellipsis != null) {
141158
cssStyle
142159
..overflowY = 'hidden'
@@ -199,15 +216,6 @@ class CanvasParagraph implements EngineParagraph {
199216
return _layoutService.getBoxesForPlaceholders();
200217
}
201218

202-
// TODO(mdebbar): Check for child spans if any has styles that can't be drawn
203-
// on a canvas. e.g:
204-
// - decoration
205-
// - word-spacing
206-
// - shadows (may be possible? https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/shadowBlur)
207-
// - font features
208-
@override
209-
final bool drawOnCanvas = true;
210-
211219
@override
212220
List<ui.TextBox> getBoxesForRange(
213221
int start,
@@ -599,13 +607,29 @@ class CanvasParagraphBuilder implements ui.ParagraphBuilder {
599607
}
600608
}
601609

610+
bool _drawOnCanvas = true;
611+
602612
@override
603613
void addText(String text) {
604614
final EngineTextStyle style = _currentStyleNode.resolveStyle();
605615
final int start = _plainTextBuffer.length;
606616
_plainTextBuffer.write(text);
607617
final int end = _plainTextBuffer.length;
608618

619+
if (_drawOnCanvas) {
620+
final ui.TextDecoration? decoration = style._decoration;
621+
if (decoration != null && decoration != ui.TextDecoration.none) {
622+
_drawOnCanvas = false;
623+
}
624+
}
625+
626+
if (_drawOnCanvas) {
627+
final List<ui.FontFeature>? fontFeatures = style._fontFeatures;
628+
if (fontFeatures != null && fontFeatures.isNotEmpty) {
629+
_drawOnCanvas = false;
630+
}
631+
}
632+
609633
_spans.add(FlatTextSpan(style: style, start: start, end: end));
610634
}
611635

@@ -616,6 +640,7 @@ class CanvasParagraphBuilder implements ui.ParagraphBuilder {
616640
paragraphStyle: _paragraphStyle,
617641
plainText: _plainTextBuffer.toString(),
618642
placeholderCount: _placeholderCount,
643+
drawOnCanvas: _drawOnCanvas,
619644
);
620645
}
621646
}

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class TextLayoutService {
2323

2424
double height = 0.0;
2525

26-
double longestLine = 0.0;
26+
EngineLineMetrics? longestLine;
2727

2828
double minIntrinsicWidth = 0.0;
2929

@@ -65,7 +65,7 @@ class TextLayoutService {
6565
// Reset results from previous layout.
6666
width = constraints.width;
6767
height = 0.0;
68-
longestLine = 0.0;
68+
longestLine = null;
6969
minIntrinsicWidth = 0.0;
7070
maxIntrinsicWidth = 0.0;
7171
didExceedMaxLines = false;
@@ -187,8 +187,9 @@ class TextLayoutService {
187187
alphabeticBaseline = line.baseline;
188188
ideographicBaseline = alphabeticBaseline * _baselineRatioHack;
189189
}
190-
if (longestLine < line.width) {
191-
longestLine = line.width;
190+
final double longestLineWidth = longestLine?.width ?? 0.0;
191+
if (longestLineWidth < line.width) {
192+
longestLine = line;
192193
}
193194
}
194195

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

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,47 @@ void testMain() async {
112112
return takeScreenshot(canvas, bounds, 'canvas_paragraph_align');
113113
});
114114

115+
test('respects alignment in DOM mode', () {
116+
final canvas = DomCanvas(domRenderer.createElement('flt-picture'));
117+
118+
Offset offset = Offset.zero;
119+
CanvasParagraph paragraph;
120+
121+
void build(CanvasParagraphBuilder builder) {
122+
builder.pushStyle(EngineTextStyle.only(color: black));
123+
builder.addText('Lorem ');
124+
builder.pushStyle(EngineTextStyle.only(color: blue));
125+
builder.addText('ipsum ');
126+
builder.pushStyle(EngineTextStyle.only(color: green));
127+
builder.addText('dolor ');
128+
builder.pushStyle(EngineTextStyle.only(color: red));
129+
builder.addText('sit');
130+
}
131+
132+
paragraph = rich(
133+
ParagraphStyle(fontFamily: 'Roboto', textAlign: TextAlign.left),
134+
build,
135+
)..layout(constrain(100.0));
136+
canvas.drawParagraph(paragraph, offset);
137+
offset = offset.translate(0, paragraph.height + 10);
138+
139+
paragraph = rich(
140+
ParagraphStyle(fontFamily: 'Roboto', textAlign: TextAlign.center),
141+
build,
142+
)..layout(constrain(100.0));
143+
canvas.drawParagraph(paragraph, offset);
144+
offset = offset.translate(0, paragraph.height + 10);
145+
146+
paragraph = rich(
147+
ParagraphStyle(fontFamily: 'Roboto', textAlign: TextAlign.right),
148+
build,
149+
)..layout(constrain(100.0));
150+
canvas.drawParagraph(paragraph, offset);
151+
offset = offset.translate(0, paragraph.height + 10);
152+
153+
return takeScreenshot(canvas, bounds, 'canvas_paragraph_align_dom');
154+
});
155+
115156
test('paints spans with varying heights/baselines', () {
116157
final canvas = BitmapCanvas(bounds, RenderStrategy());
117158

@@ -165,4 +206,37 @@ void testMain() async {
165206

166207
return takeScreenshot(canvas, bounds, 'canvas_paragraph_letter_spacing');
167208
});
209+
210+
test('draws text decorations', () {
211+
final canvas = BitmapCanvas(bounds, RenderStrategy());
212+
final List<TextDecorationStyle> decorationStyles = <TextDecorationStyle>[
213+
TextDecorationStyle.solid,
214+
TextDecorationStyle.double,
215+
TextDecorationStyle.dotted,
216+
TextDecorationStyle.dashed,
217+
TextDecorationStyle.wavy,
218+
];
219+
220+
final CanvasParagraph paragraph = rich(
221+
ParagraphStyle(fontFamily: 'Roboto'),
222+
(builder) {
223+
for (TextDecorationStyle decorationStyle in decorationStyles) {
224+
builder.pushStyle(EngineTextStyle.only(
225+
color: const Color.fromRGBO(50, 50, 255, 1.0),
226+
decoration: TextDecoration.underline,
227+
decorationStyle: decorationStyle,
228+
decorationColor: red,
229+
fontFamily: 'Roboto',
230+
fontSize: 30,
231+
));
232+
builder.addText('Hello World');
233+
builder.pop();
234+
builder.addText(' ');
235+
}
236+
},
237+
)..layout(constrain(double.infinity));
238+
239+
canvas.drawParagraph(paragraph, Offset.zero);
240+
return takeScreenshot(canvas, bounds, 'canvas_paragraph_decoration');
241+
});
168242
}

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,40 @@ void testMain() async {
9595

9696
return takeScreenshot(canvas, bounds, 'canvas_paragraph_placeholders_align');
9797
});
98+
99+
test('draws paragraphs with placeholders and text align in DOM mode', () {
100+
final canvas = DomCanvas(domRenderer.createElement('flt-picture'));
101+
102+
const List<TextAlign> aligns = <TextAlign>[
103+
TextAlign.left,
104+
TextAlign.center,
105+
TextAlign.right,
106+
];
107+
108+
Offset offset = Offset.zero;
109+
for (TextAlign align in aligns) {
110+
final CanvasParagraph paragraph = rich(
111+
ParagraphStyle(fontFamily: 'Roboto', fontSize: 14.0, textAlign: align),
112+
(builder) {
113+
builder.pushStyle(TextStyle(color: black));
114+
builder.addText('Lorem');
115+
builder.addPlaceholder(80.0, 50.0, PlaceholderAlignment.bottom);
116+
builder.pushStyle(TextStyle(color: blue));
117+
builder.addText('ipsum.');
118+
},
119+
)..layout(constrain(200.0));
120+
121+
// Draw the paragraph.
122+
canvas.drawParagraph(paragraph, offset);
123+
124+
// 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);
128+
129+
offset = offset.translate(0.0, paragraph.height + 30.0);
130+
}
131+
132+
return takeScreenshot(canvas, bounds, 'canvas_paragraph_placeholders_align_dom');
133+
});
98134
}

0 commit comments

Comments
 (0)