diff --git a/lib/web_ui/dev/goldens_lock.yaml b/lib/web_ui/dev/goldens_lock.yaml
index 1b80d23509540..310fd510023c1 100644
--- a/lib/web_ui/dev/goldens_lock.yaml
+++ b/lib/web_ui/dev/goldens_lock.yaml
@@ -1,2 +1,2 @@
repository: https://github.com/flutter/goldens.git
-revision: b85f9093e6bc6d4e7cbb7f97491667c143c4a360
+revision: 4c3d34f19045a1df10757e5b3cb6c9ace9a6038c
diff --git a/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart b/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart
index af41960ec6c12..305ce52544fac 100644
--- a/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart
+++ b/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart
@@ -20,6 +20,7 @@ class CanvasParagraph implements EngineParagraph {
required this.paragraphStyle,
required this.plainText,
required this.placeholderCount,
+ required this.drawOnCanvas,
});
/// The flat list of spans that make up this paragraph.
@@ -34,6 +35,9 @@ class CanvasParagraph implements EngineParagraph {
/// The number of placeholders in this paragraph.
final int placeholderCount;
+ @override
+ final bool drawOnCanvas;
+
@override
double get width => _layoutService.width;
@@ -41,7 +45,7 @@ class CanvasParagraph implements EngineParagraph {
double get height => _layoutService.height;
@override
- double get longestLine => _layoutService.longestLine;
+ double get longestLine => _layoutService.longestLine?.width ?? 0.0;
@override
double get minIntrinsicWidth => _layoutService.minIntrinsicWidth;
@@ -124,6 +128,14 @@ class CanvasParagraph implements EngineParagraph {
return domElement.clone(true) as html.HtmlElement;
}
+ double _getParagraphAlignOffset() {
+ final EngineLineMetrics? longestLine = _layoutService.longestLine;
+ if (longestLine != null) {
+ return longestLine.left;
+ }
+ return 0.0;
+ }
+
html.HtmlElement _createDomElement() {
final html.HtmlElement rootElement =
domRenderer.createElement('p') as html.HtmlElement;
@@ -137,6 +149,11 @@ class CanvasParagraph implements EngineParagraph {
// to insert our own
breaks based on layout results.
..whiteSpace = 'pre';
+ final double alignOffset = _getParagraphAlignOffset();
+ if (alignOffset != 0.0) {
+ cssStyle.marginLeft = '${alignOffset}px';
+ }
+
if (paragraphStyle._maxLines != null || paragraphStyle._ellipsis != null) {
cssStyle
..overflowY = 'hidden'
@@ -199,15 +216,6 @@ class CanvasParagraph implements EngineParagraph {
return _layoutService.getBoxesForPlaceholders();
}
- // TODO(mdebbar): Check for child spans if any has styles that can't be drawn
- // on a canvas. e.g:
- // - decoration
- // - word-spacing
- // - shadows (may be possible? https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/shadowBlur)
- // - font features
- @override
- final bool drawOnCanvas = true;
-
@override
List getBoxesForRange(
int start,
@@ -599,6 +607,8 @@ class CanvasParagraphBuilder implements ui.ParagraphBuilder {
}
}
+ bool _drawOnCanvas = true;
+
@override
void addText(String text) {
final EngineTextStyle style = _currentStyleNode.resolveStyle();
@@ -606,6 +616,20 @@ class CanvasParagraphBuilder implements ui.ParagraphBuilder {
_plainTextBuffer.write(text);
final int end = _plainTextBuffer.length;
+ if (_drawOnCanvas) {
+ final ui.TextDecoration? decoration = style._decoration;
+ if (decoration != null && decoration != ui.TextDecoration.none) {
+ _drawOnCanvas = false;
+ }
+ }
+
+ if (_drawOnCanvas) {
+ final List? fontFeatures = style._fontFeatures;
+ if (fontFeatures != null && fontFeatures.isNotEmpty) {
+ _drawOnCanvas = false;
+ }
+ }
+
_spans.add(FlatTextSpan(style: style, start: start, end: end));
}
@@ -616,6 +640,7 @@ class CanvasParagraphBuilder implements ui.ParagraphBuilder {
paragraphStyle: _paragraphStyle,
plainText: _plainTextBuffer.toString(),
placeholderCount: _placeholderCount,
+ drawOnCanvas: _drawOnCanvas,
);
}
}
diff --git a/lib/web_ui/lib/src/engine/text/layout_service.dart b/lib/web_ui/lib/src/engine/text/layout_service.dart
index 120b38c6d406a..57cf525004f71 100644
--- a/lib/web_ui/lib/src/engine/text/layout_service.dart
+++ b/lib/web_ui/lib/src/engine/text/layout_service.dart
@@ -23,7 +23,7 @@ class TextLayoutService {
double height = 0.0;
- double longestLine = 0.0;
+ EngineLineMetrics? longestLine;
double minIntrinsicWidth = 0.0;
@@ -65,7 +65,7 @@ class TextLayoutService {
// Reset results from previous layout.
width = constraints.width;
height = 0.0;
- longestLine = 0.0;
+ longestLine = null;
minIntrinsicWidth = 0.0;
maxIntrinsicWidth = 0.0;
didExceedMaxLines = false;
@@ -187,8 +187,9 @@ class TextLayoutService {
alphabeticBaseline = line.baseline;
ideographicBaseline = alphabeticBaseline * _baselineRatioHack;
}
- if (longestLine < line.width) {
- longestLine = line.width;
+ final double longestLineWidth = longestLine?.width ?? 0.0;
+ if (longestLineWidth < line.width) {
+ longestLine = line;
}
}
diff --git a/lib/web_ui/test/golden_tests/engine/canvas_paragraph/general_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_paragraph/general_test.dart
index ba9cb5a74b13b..7a2d7dcc1b614 100644
--- a/lib/web_ui/test/golden_tests/engine/canvas_paragraph/general_test.dart
+++ b/lib/web_ui/test/golden_tests/engine/canvas_paragraph/general_test.dart
@@ -112,6 +112,47 @@ void testMain() async {
return takeScreenshot(canvas, bounds, 'canvas_paragraph_align');
});
+ test('respects alignment in DOM mode', () {
+ final canvas = DomCanvas(domRenderer.createElement('flt-picture'));
+
+ Offset offset = Offset.zero;
+ CanvasParagraph paragraph;
+
+ void build(CanvasParagraphBuilder builder) {
+ builder.pushStyle(EngineTextStyle.only(color: black));
+ builder.addText('Lorem ');
+ builder.pushStyle(EngineTextStyle.only(color: blue));
+ builder.addText('ipsum ');
+ builder.pushStyle(EngineTextStyle.only(color: green));
+ builder.addText('dolor ');
+ builder.pushStyle(EngineTextStyle.only(color: red));
+ builder.addText('sit');
+ }
+
+ paragraph = rich(
+ ParagraphStyle(fontFamily: 'Roboto', textAlign: TextAlign.left),
+ build,
+ )..layout(constrain(100.0));
+ canvas.drawParagraph(paragraph, offset);
+ offset = offset.translate(0, paragraph.height + 10);
+
+ paragraph = rich(
+ ParagraphStyle(fontFamily: 'Roboto', textAlign: TextAlign.center),
+ build,
+ )..layout(constrain(100.0));
+ canvas.drawParagraph(paragraph, offset);
+ offset = offset.translate(0, paragraph.height + 10);
+
+ paragraph = rich(
+ ParagraphStyle(fontFamily: 'Roboto', textAlign: TextAlign.right),
+ build,
+ )..layout(constrain(100.0));
+ canvas.drawParagraph(paragraph, offset);
+ offset = offset.translate(0, paragraph.height + 10);
+
+ return takeScreenshot(canvas, bounds, 'canvas_paragraph_align_dom');
+ });
+
test('paints spans with varying heights/baselines', () {
final canvas = BitmapCanvas(bounds, RenderStrategy());
@@ -165,4 +206,37 @@ void testMain() async {
return takeScreenshot(canvas, bounds, 'canvas_paragraph_letter_spacing');
});
+
+ test('draws text decorations', () {
+ final canvas = BitmapCanvas(bounds, RenderStrategy());
+ final List decorationStyles = [
+ TextDecorationStyle.solid,
+ TextDecorationStyle.double,
+ TextDecorationStyle.dotted,
+ TextDecorationStyle.dashed,
+ TextDecorationStyle.wavy,
+ ];
+
+ final CanvasParagraph paragraph = rich(
+ ParagraphStyle(fontFamily: 'Roboto'),
+ (builder) {
+ for (TextDecorationStyle decorationStyle in decorationStyles) {
+ builder.pushStyle(EngineTextStyle.only(
+ color: const Color.fromRGBO(50, 50, 255, 1.0),
+ decoration: TextDecoration.underline,
+ decorationStyle: decorationStyle,
+ decorationColor: red,
+ fontFamily: 'Roboto',
+ fontSize: 30,
+ ));
+ builder.addText('Hello World');
+ builder.pop();
+ builder.addText(' ');
+ }
+ },
+ )..layout(constrain(double.infinity));
+
+ canvas.drawParagraph(paragraph, Offset.zero);
+ return takeScreenshot(canvas, bounds, 'canvas_paragraph_decoration');
+ });
}
diff --git a/lib/web_ui/test/golden_tests/engine/canvas_paragraph/placeholders_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_paragraph/placeholders_test.dart
index fbede70d3b8c7..45440b03dc3a9 100644
--- a/lib/web_ui/test/golden_tests/engine/canvas_paragraph/placeholders_test.dart
+++ b/lib/web_ui/test/golden_tests/engine/canvas_paragraph/placeholders_test.dart
@@ -95,4 +95,40 @@ void testMain() async {
return takeScreenshot(canvas, bounds, 'canvas_paragraph_placeholders_align');
});
+
+ test('draws paragraphs with placeholders and text align in DOM mode', () {
+ final canvas = DomCanvas(domRenderer.createElement('flt-picture'));
+
+ const List aligns = [
+ TextAlign.left,
+ TextAlign.center,
+ TextAlign.right,
+ ];
+
+ Offset offset = Offset.zero;
+ for (TextAlign align in aligns) {
+ final CanvasParagraph paragraph = rich(
+ ParagraphStyle(fontFamily: 'Roboto', fontSize: 14.0, textAlign: align),
+ (builder) {
+ builder.pushStyle(TextStyle(color: black));
+ builder.addText('Lorem');
+ builder.addPlaceholder(80.0, 50.0, PlaceholderAlignment.bottom);
+ builder.pushStyle(TextStyle(color: blue));
+ builder.addText('ipsum.');
+ },
+ )..layout(constrain(200.0));
+
+ // Draw the paragraph.
+ 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);
+
+ offset = offset.translate(0.0, paragraph.height + 30.0);
+ }
+
+ return takeScreenshot(canvas, bounds, 'canvas_paragraph_placeholders_align_dom');
+ });
}