diff --git a/lib/web_ui/dev/goldens_lock.yaml b/lib/web_ui/dev/goldens_lock.yaml index b2ba801a9d4c5..75c3c349ca9ea 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: 0dd2e82050422c05e9daf652fa4267fb7c01f260 +revision: 0e62a287b5109053bc3e59dd8fc832f488d904c9 diff --git a/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart b/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart index e856684c14e10..dfc220bdfbf57 100644 --- a/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart +++ b/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart @@ -916,7 +916,7 @@ class BitmapCanvas extends EngineCanvas { /// /// The text is drawn starting at coordinates ([x], [y]). It uses the current /// font set by the most recent call to [setCssFont]. - void fillText(String text, double x, double y, {List? shadows}) { + void drawText(String text, double x, double y, {ui.PaintingStyle? style, List? shadows}) { final html.CanvasRenderingContext2D ctx = _canvasPool.context; if (shadows != null) { ctx.save(); @@ -926,11 +926,20 @@ class BitmapCanvas extends EngineCanvas { ctx.shadowOffsetX = shadow.offset.dx; ctx.shadowOffsetY = shadow.offset.dy; - ctx.fillText(text, x, y); + if (style == ui.PaintingStyle.stroke) { + ctx.strokeText(text, x, y); + } else { + ctx.fillText(text, x, y); + } } ctx.restore(); } - ctx.fillText(text, x, y); + + if (style == ui.PaintingStyle.stroke) { + ctx.strokeText(text, x, y); + } else { + ctx.fillText(text, x, y); + } } @override diff --git a/lib/web_ui/lib/src/engine/text/paint_service.dart b/lib/web_ui/lib/src/engine/text/paint_service.dart index 2d57b5e13ff2b..524c6a12db4ce 100644 --- a/lib/web_ui/lib/src/engine/text/paint_service.dart +++ b/lib/web_ui/lib/src/engine/text/paint_service.dart @@ -103,7 +103,8 @@ class TextPaintService { ); final double? letterSpacing = span.style.letterSpacing; if (letterSpacing == null || letterSpacing == 0.0) { - canvas.fillText(text, x, y, shadows: span.style.shadows); + canvas.drawText(text, x, y, + style: span.style.foreground?.style, shadows: span.style.shadows); } else { // TODO(mdebbar): Implement letter-spacing on canvas more efficiently: // https://github.com/flutter/flutter/issues/51234 @@ -111,7 +112,8 @@ class TextPaintService { final int len = text.length; for (int i = 0; i < len; i++) { final String char = text[i]; - canvas.fillText(char, charX.roundToDouble(), y, + canvas.drawText(char, charX.roundToDouble(), y, + style: span.style.foreground?.style, shadows: span.style.shadows); charX += letterSpacing + canvas.measureText(char).width!; } @@ -122,7 +124,7 @@ class TextPaintService { final String? ellipsis = line.ellipsis; if (ellipsis != null && box == line.boxes.last) { final double x = offset.dx + line.left + box.right; - canvas.fillText(ellipsis, x, y); + canvas.drawText(ellipsis, x, y, style: span.style.foreground?.style); } canvas.tearDownPaint(); diff --git a/lib/web_ui/lib/src/engine/text/paragraph.dart b/lib/web_ui/lib/src/engine/text/paragraph.dart index 78829587a5df6..a74657951bd2e 100644 --- a/lib/web_ui/lib/src/engine/text/paragraph.dart +++ b/lib/web_ui/lib/src/engine/text/paragraph.dart @@ -696,7 +696,21 @@ void applyTextStyleToElement({ final html.CssStyleDeclaration cssStyle = element.style; final ui.Color? color = style.foreground?.color ?? style.color; - if (color != null) { + if (style.foreground?.style == ui.PaintingStyle.stroke) { + // When comparing the outputs of the Bitmap Canvas and the DOM + // implementation, we have found, that we need to set the background color + // of the text to transparent to achieve the same effect as in the Bitmap + // Canvas and the Skia Engine where only the text stroke is painted. + // If we don't set it here to transparent, the text will inherit the color + // of it's parent element. + cssStyle.color = 'transparent'; + // Use hairline (device pixel when strokeWidth is not specified). + final double? strokeWidth = style.foreground?.strokeWidth; + final double adaptedWidth = strokeWidth != null && strokeWidth > 0 + ? strokeWidth + : 1.0 / ui.window.devicePixelRatio; + cssStyle.textStroke = '${adaptedWidth}px ${colorToCssString(color)}'; + } else if (color != null) { cssStyle.color = colorToCssString(color); } final ui.Color? background = style.background?.color; diff --git a/lib/web_ui/test/html/paragraph/general_golden_test.dart b/lib/web_ui/test/html/paragraph/general_golden_test.dart index 261c452a075ac..734d8eb65f2f4 100644 --- a/lib/web_ui/test/html/paragraph/general_golden_test.dart +++ b/lib/web_ui/test/html/paragraph/general_golden_test.dart @@ -502,4 +502,42 @@ Future testMain() async { testBackgroundStyle(canvas); return takeScreenshot(canvas, bounds, 'canvas_paragraph_background_style_dom'); }); + + void testForegroundStyle(EngineCanvas canvas) { + final CanvasParagraph paragraph = rich( + EngineParagraphStyle(fontFamily: 'Roboto', fontSize: 40.0), + (CanvasParagraphBuilder builder) { + builder.pushStyle(EngineTextStyle.only(color: blue)); + builder.addText('Lorem'); + builder.pop(); + builder.pushStyle(EngineTextStyle.only(foreground: Paint()..color = red..style = PaintingStyle.stroke)); + builder.addText('ipsum\n'); + builder.pop(); + builder.pushStyle(EngineTextStyle.only(foreground: Paint()..color = blue..style = PaintingStyle.stroke..strokeWidth = 0.0)); + builder.addText('dolor'); + builder.pop(); + builder.pushStyle(EngineTextStyle.only(foreground: Paint()..color = green..style = PaintingStyle.stroke..strokeWidth = 2.0)); + builder.addText('sit\n'); + builder.pop(); + builder.pushStyle(EngineTextStyle.only(foreground: Paint()..color = yellow..style = PaintingStyle.stroke..strokeWidth = 4.0)); + builder.addText('amet'); + }, + ); + paragraph.layout(constrain(double.infinity)); + canvas.drawParagraph(paragraph, Offset.zero); + } + + test('foreground style', () { + const Rect bounds = Rect.fromLTWH(0, 0, 300, 200); + final BitmapCanvas canvas = BitmapCanvas(bounds, RenderStrategy()); + testForegroundStyle(canvas); + return takeScreenshot(canvas, bounds, 'canvas_paragraph_foreground_style'); + }); + + test('foreground style (DOM)', () { + const Rect bounds = Rect.fromLTWH(0, 0, 300, 200); + final DomCanvas canvas = DomCanvas(domRenderer.createElement('flt-picture')); + testForegroundStyle(canvas); + return takeScreenshot(canvas, bounds, 'canvas_paragraph_foreground_style_dom'); + }); }