From b0160f10ce010c13ea387661434578ccefc0c51e Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Tue, 26 Jan 2021 11:04:11 -0800 Subject: [PATCH 1/2] [web] Fix alignment issue in rich paragraphs --- .../lib/src/engine/text/canvas_paragraph.dart | 45 ++++++++--- .../lib/src/engine/text/layout_service.dart | 9 ++- .../engine/canvas_paragraph/general_test.dart | 74 +++++++++++++++++++ .../canvas_paragraph/placeholders_test.dart | 36 +++++++++ 4 files changed, 150 insertions(+), 14 deletions(-) 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'); + }); } From 14d6e8114be3c94be582200ee8ea568651d870ae Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Tue, 26 Jan 2021 12:05:03 -0800 Subject: [PATCH 2/2] update goldens lock --- lib/web_ui/dev/goldens_lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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