diff --git a/lib/web_ui/lib/src/engine/bitmap_canvas.dart b/lib/web_ui/lib/src/engine/bitmap_canvas.dart index 719737c8aa692..3f6d0e3874af5 100644 --- a/lib/web_ui/lib/src/engine/bitmap_canvas.dart +++ b/lib/web_ui/lib/src/engine/bitmap_canvas.dart @@ -771,70 +771,37 @@ class BitmapCanvas extends EngineCanvas { _childOverdraw = true; } - void _drawTextLine( - ParagraphGeometricStyle style, - EngineLineMetrics line, - double x, - double y, - ) { - html.CanvasRenderingContext2D ctx = _canvasPool.context; - x += line.left; - final double? letterSpacing = style.letterSpacing; - if (letterSpacing == null || letterSpacing == 0.0) { - ctx.fillText(line.displayText!, x, y); - } else { - // When letter-spacing is set, we go through a more expensive code path - // that renders each character separately with the correct spacing - // between them. - // - // We are drawing letter spacing like the web does it, by adding the - // spacing after each letter. This is different from Flutter which puts - // the spacing around each letter i.e. for a 10px letter spacing, Flutter - // would put 5px before each letter and 5px after it, but on the web, we - // put no spacing before the letter and 10px after it. This is how the DOM - // does it. - final int len = line.displayText!.length; - for (int i = 0; i < len; i++) { - final String char = line.displayText![i]; - ctx.fillText(char, x, y); - x += letterSpacing + ctx.measureText(char).width!; - } + void setFontFromParagraphStyle(ParagraphGeometricStyle style) { + if (style != _cachedLastStyle) { + html.CanvasRenderingContext2D ctx = _canvasPool.context; + ctx.font = style.cssFontString; + _cachedLastStyle = style; } } + /// Measures the given [text] and returns a [html.TextMetrics] object that + /// contains information about the measurement. + /// + /// The text is measured using the font set by the most recent call to + /// [setFontFromParagraphStyle]. + html.TextMetrics measureText(String text) { + return _canvasPool.context.measureText(text); + } + + /// Draws text to the canvas starting at coordinate ([x], [y]). + /// + /// The text is drawn starting at coordinates ([x], [y]). It uses the current + /// font set by the most recent call to [setFontFromParagraphStyle]. + void fillText(String text, double x, double y) { + _canvasPool.context.fillText(text, x, y); + } + @override void drawParagraph(EngineParagraph paragraph, ui.Offset offset) { - assert(paragraph._isLaidOut); - final ParagraphGeometricStyle style = paragraph._geometricStyle; - - if (paragraph._drawOnCanvas && _childOverdraw == false) { - // !Do not move this assignment above this if clause since, accessing - // context will generate extra tags. - final List lines = - paragraph._measurementResult!.lines!; - - final SurfacePaintData? backgroundPaint = - paragraph._background?.paintData; - if (backgroundPaint != null) { - final ui.Rect rect = ui.Rect.fromLTWH( - offset.dx, offset.dy, paragraph.width, paragraph.height); - drawRect(rect, backgroundPaint); - } - - if (style != _cachedLastStyle) { - html.CanvasRenderingContext2D ctx = _canvasPool.context; - ctx.font = style.cssFontString; - _cachedLastStyle = style; - } - _setUpPaint(paragraph._paint!.paintData, null); - double y = offset.dy + paragraph.alphabeticBaseline; - final int len = lines.length; - for (int i = 0; i < len; i++) { - _drawTextLine(style, lines[i], offset.dx, y); - y += paragraph._lineHeight; - } - _tearDownPaint(); + assert(paragraph.isLaidOut); + if (paragraph.drawOnCanvas && _childOverdraw == false) { + paragraph.paint(this, offset); return; } diff --git a/lib/web_ui/lib/src/engine/engine_canvas.dart b/lib/web_ui/lib/src/engine/engine_canvas.dart index c197515900a67..955bac1266ee1 100644 --- a/lib/web_ui/lib/src/engine/engine_canvas.dart +++ b/lib/web_ui/lib/src/engine/engine_canvas.dart @@ -251,18 +251,12 @@ html.Element _drawParagraphElement( ui.Offset offset, { Matrix4? transform, }) { - assert(paragraph._isLaidOut); + assert(paragraph.isLaidOut); - final html.Element paragraphElement = paragraph._paragraphElement.clone(true) as html.Element; - - final html.CssStyleDeclaration paragraphStyle = paragraphElement.style; - paragraphStyle - ..position = 'absolute' - ..whiteSpace = 'pre-wrap' - ..overflowWrap = 'break-word' - ..overflow = 'hidden' - ..height = '${paragraph.height}px' - ..width = '${paragraph.width}px'; + final html.HtmlElement paragraphElement = paragraph.toDomElement(); + paragraphElement.style + ..height = '${paragraph.height}px' + ..width = '${paragraph.width}px'; if (transform != null) { setElementTransform( @@ -270,16 +264,6 @@ html.Element _drawParagraphElement( transformWithOffset(transform, offset).storage, ); } - - final ParagraphGeometricStyle style = paragraph._geometricStyle; - - // TODO(flutter_web): https://github.com/flutter/flutter/issues/33223 - if (style.ellipsis != null && - (style.maxLines == null || style.maxLines == 1)) { - paragraphStyle - ..whiteSpace = 'pre' - ..textOverflow = 'ellipsis'; - } return paragraphElement; } diff --git a/lib/web_ui/lib/src/engine/html/recording_canvas.dart b/lib/web_ui/lib/src/engine/html/recording_canvas.dart index 9f7e6b93bca92..be5e6a6a4b21b 100644 --- a/lib/web_ui/lib/src/engine/html/recording_canvas.dart +++ b/lib/web_ui/lib/src/engine/html/recording_canvas.dart @@ -522,13 +522,13 @@ class RecordingCanvas { void drawParagraph(ui.Paragraph paragraph, ui.Offset offset) { assert(!_recordingEnded); final EngineParagraph engineParagraph = paragraph as EngineParagraph; - if (!engineParagraph._isLaidOut) { + if (!engineParagraph.isLaidOut) { // Ignore non-laid out paragraphs. This matches Flutter's behavior. return; } _didDraw = true; - if (engineParagraph._geometricStyle.ellipsis != null) { + if (engineParagraph.hasArbitraryPaint) { _hasArbitraryPaint = true; } final double left = offset.dx; diff --git a/lib/web_ui/lib/src/engine/text/paragraph.dart b/lib/web_ui/lib/src/engine/text/paragraph.dart index c38695ccb1b72..8f58cfec5fa0e 100644 --- a/lib/web_ui/lib/src/engine/text/paragraph.dart +++ b/lib/web_ui/lib/src/engine/text/paragraph.dart @@ -335,9 +335,94 @@ class EngineParagraph implements ui.Paragraph { } } + bool get hasArbitraryPaint => _geometricStyle.ellipsis != null; + + void paint(BitmapCanvas canvas, ui.Offset offset) { + assert(drawOnCanvas); + assert(isLaidOut); + + // Paint the background first. + final SurfacePaint? background = _background; + if (background != null) { + final ui.Rect rect = ui.Rect.fromLTWH(offset.dx, offset.dy, width, height); + canvas.drawRect(rect, background.paintData); + } + + final List lines = _measurementResult!.lines!; + canvas.setFontFromParagraphStyle(_geometricStyle); + + // Then paint the text. + canvas._setUpPaint(_paint!.paintData, null); + double y = offset.dy + alphabeticBaseline; + final int len = lines.length; + for (int i = 0; i < len; i++) { + _paintLine(canvas, lines[i], offset.dx, y); + y += _lineHeight; + } + canvas._tearDownPaint(); + } + + void _paintLine( + BitmapCanvas canvas, + EngineLineMetrics line, + double x, + double y, + ) { + x += line.left; + final double? letterSpacing = _geometricStyle.letterSpacing; + if (letterSpacing == null || letterSpacing == 0.0) { + canvas.fillText(line.displayText!, x, y); + } else { + // When letter-spacing is set, we go through a more expensive code path + // that renders each character separately with the correct spacing + // between them. + // + // We are drawing letter spacing like the web does it, by adding the + // spacing after each letter. This is different from Flutter which puts + // the spacing around each letter i.e. for a 10px letter spacing, Flutter + // would put 5px before each letter and 5px after it, but on the web, we + // put no spacing before the letter and 10px after it. This is how the DOM + // does it. + // + // TODO(mdebbar): Implement letter-spacing on canvas more efficiently: + // https://github.com/flutter/flutter/issues/51234 + final int len = line.displayText!.length; + for (int i = 0; i < len; i++) { + final String char = line.displayText![i]; + canvas.fillText(char, x, y); + x += letterSpacing + canvas.measureText(char).width!; + } + } + } + + html.HtmlElement toDomElement() { + assert(isLaidOut); + + final html.HtmlElement paragraphElement = + _paragraphElement.clone(true) as html.HtmlElement; + + final html.CssStyleDeclaration paragraphStyle = paragraphElement.style; + paragraphStyle + ..position = 'absolute' + ..whiteSpace = 'pre-wrap' + ..overflowWrap = 'break-word' + ..overflow = 'hidden'; + + final ParagraphGeometricStyle style = _geometricStyle; + + // TODO(flutter_web): https://github.com/flutter/flutter/issues/33223 + if (style.ellipsis != null && + (style.maxLines == null || style.maxLines == 1)) { + paragraphStyle + ..whiteSpace = 'pre' + ..textOverflow = 'ellipsis'; + } + return paragraphElement; + } + @override List getBoxesForPlaceholders() { - assert(_isLaidOut); + assert(isLaidOut); return _measurementResult!.placeholderBoxes; } @@ -351,7 +436,7 @@ class EngineParagraph implements ui.Paragraph { /// - Paragraphs that contain decorations. /// - Paragraphs that have a non-null word-spacing. /// - Paragraphs with a background. - bool get _drawOnCanvas { + bool get drawOnCanvas { if (!_hasLineMetrics) { return false; } @@ -370,7 +455,7 @@ class EngineParagraph implements ui.Paragraph { } /// Whether this paragraph has been laid out. - bool get _isLaidOut => _measurementResult != null; + bool get isLaidOut => _measurementResult != null; /// Asserts that the properties used to measure paragraph layout are the same /// as the properties of this paragraphs root style.