From 1e11b7f3c365e6b30a9d275b43066845fd8f8889 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Wed, 28 Oct 2020 13:35:30 -0700 Subject: [PATCH 1/3] [web] Split the EngineParagraph interface from the legacy implementation --- lib/web_ui/lib/src/engine.dart | 1 + .../lib/src/engine/html/recording_canvas.dart | 2 +- .../lib/src/engine/text/canvas_paragraph.dart | 429 ++++++++++++++++++ .../lib/src/engine/text/measurement.dart | 34 +- lib/web_ui/lib/src/engine/text/paragraph.dart | 72 ++- lib/web_ui/lib/src/engine/text/ruler.dart | 10 +- lib/web_ui/lib/src/ui/text.dart | 2 +- .../text/canvas_paragraph_builder_test.dart | 156 +++++++ 8 files changed, 669 insertions(+), 37 deletions(-) create mode 100644 lib/web_ui/lib/src/engine/text/canvas_paragraph.dart create mode 100644 lib/web_ui/test/text/canvas_paragraph_builder_test.dart diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index 53d778fb91cc6..46a217bbc35de 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -129,6 +129,7 @@ part 'engine/text/line_break_properties.dart'; part 'engine/text/line_breaker.dart'; part 'engine/text/measurement.dart'; part 'engine/text/paragraph.dart'; +part 'engine/text/canvas_paragraph.dart'; part 'engine/text/ruler.dart'; part 'engine/text/unicode_range.dart'; part 'engine/text/word_break_properties.dart'; 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 be5e6a6a4b21b..f3b7d253ce305 100644 --- a/lib/web_ui/lib/src/engine/html/recording_canvas.dart +++ b/lib/web_ui/lib/src/engine/html/recording_canvas.dart @@ -1185,7 +1185,7 @@ class PaintDrawParagraph extends DrawCommand { @override String toString() { if (assertionsEnabled) { - return 'DrawParagraph(${paragraph._plainText}, $offset)'; + return 'DrawParagraph(${paragraph.toPlainText()}, $offset)'; } else { return super.toString(); } diff --git a/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart b/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart new file mode 100644 index 0000000000000..5f7d54c895567 --- /dev/null +++ b/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart @@ -0,0 +1,429 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.10 +part of engine; + +/// A paragraph made up of a flat list of text spans and placeholders. +/// +/// As opposed to [DomParagraph], a [CanvasParagraph] doesn't use a DOM element +/// to represent the structure of its spans and styles. Instead it uses a flat +/// list of [ParagraphSpan] objects. +class CanvasParagraph implements EngineParagraph { + /// This class is created by the engine, and should not be instantiated + /// or extended directly. + /// + /// To create a [CanvasParagraph] object, use a [CanvasParagraphBuilder]. + CanvasParagraph( + this.spans, { + required this.paragraphStyle, + required this.placeholderCount, + }); + + /// The flat list of spans that make up this paragraph. + final List spans; + + /// General styling information for this paragraph. + final EngineParagraphStyle paragraphStyle; + + /// The number of placeholders in this paragraph. + final int placeholderCount; + + @override + double width = -1.0; + + @override + double height = 0.0; + + @override + double get longestLine { + assert(isLaidOut); + // TODO(mdebbar): Use the line metrics generated during layout to find out + // the longest line. + return 0.0; + } + + @override + double minIntrinsicWidth = 0.0; + + @override + double maxIntrinsicWidth = 0.0; + + @override + double alphabeticBaseline = -1.0; + + @override + double ideographicBaseline = -1.0; + + @override + bool get didExceedMaxLines => _didExceedMaxLines; + bool _didExceedMaxLines = false; + + ui.ParagraphConstraints? _lastUsedConstraints; + + @override + void layout(ui.ParagraphConstraints constraints) { + // When constraint width has a decimal place, we floor it to avoid getting + // a layout width that's higher than the constraint width. + // + // For example, if constraint width is `30.8` and the text has a width of + // `30.5` then the TextPainter in the framework will ceil the `30.5` width + // which will result in a width of `40.0` that's higher than the constraint + // width. + constraints = ui.ParagraphConstraints( + width: constraints.width.floorToDouble(), + ); + + if (constraints == _lastUsedConstraints) { + return; + } + + late Stopwatch stopwatch; + if (Profiler.isBenchmarkMode) { + stopwatch = Stopwatch()..start(); + } + // TODO(mdebbar): Perform the layout using a new rich text measurement service. + // TODO(mdebbar): Don't forget to update `_didExceedMaxLines`. + if (Profiler.isBenchmarkMode) { + stopwatch.stop(); + Profiler.instance + .benchmark('text_layout', stopwatch.elapsedMicroseconds.toDouble()); + } + + _lastUsedConstraints = constraints; + } + + // TODO(mdebbar): Returning true means we always require a bitmap canvas. Revisit + // this decision once `CanvasParagraph` is fully implemented. + @override + bool get hasArbitraryPaint => true; + + @override + void paint(BitmapCanvas canvas, ui.Offset offset) { + // TODO(mdebbar): Loop through the spans and for each box in the span: + // 1. Paint the background rect. + // 2. Paint the text shadows? + // 3. Paint the text. + } + + String? _cachedPlainText; + + @override + String toPlainText() { + final String? plainText = _cachedPlainText; + if (plainText == null) { + return _cachedPlainText ??= _computePlainText(); + } + return plainText; + } + + String _computePlainText() { + final StringBuffer buffer = StringBuffer(); + for (final ParagraphSpan span in spans) { + if (span is FlatTextSpan) { + buffer.write(span.text); + } + } + return buffer.toString(); + } + + html.HtmlElement? _cachedDomElement; + + @override + html.HtmlElement toDomElement() { + final html.HtmlElement? domElement = _cachedDomElement; + if (domElement == null) { + return _cachedDomElement ??= _createDomElement(); + } + return domElement.clone(true) as html.HtmlElement; + } + + html.HtmlElement _createDomElement() { + final html.HtmlElement element = + domRenderer.createElement('p') as html.HtmlElement; + + // 1. Set paragraph-level styles. + final html.CssStyleDeclaration cssStyle = element.style; + final ui.TextDirection direction = + paragraphStyle._textDirection ?? ui.TextDirection.ltr; + final ui.TextAlign align = paragraphStyle._textAlign ?? ui.TextAlign.start; + cssStyle + ..direction = _textDirectionToCss(direction) + ..textAlign = textAlignToCssValue(align, direction) + ..position = 'absolute' + ..whiteSpace = 'pre-wrap' + ..overflowWrap = 'break-word' + ..overflow = 'hidden'; + + if (paragraphStyle._ellipsis != null && + (paragraphStyle._maxLines == null || paragraphStyle._maxLines == 1)) { + cssStyle + ..whiteSpace = 'pre' + ..textOverflow = 'ellipsis'; + } + + // 2. Append all spans to the paragraph. + for (final ParagraphSpan span in spans) { + if (span is FlatTextSpan) { + final html.HtmlElement spanElement = + domRenderer.createElement('span') as html.HtmlElement; + _applyTextStyleToElement( + element: spanElement, + style: span.style, + isSpan: true, + ); + domRenderer.append(element, spanElement); + } else if (span is ParagraphPlaceholder) { + domRenderer.append( + element, + _createPlaceholderElement(placeholder: span), + ); + } + } + return element; + } + + @override + List getBoxesForPlaceholders() { + // TODO(mdebbar): After layout, placeholders positions should've been + // determined and can be used to compute their boxes. + return []; + } + + // 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 + bool isLaidOut = false; + + @override + List getBoxesForRange( + int start, + int end, { + ui.BoxHeightStyle boxHeightStyle = ui.BoxHeightStyle.tight, + ui.BoxWidthStyle boxWidthStyle = ui.BoxWidthStyle.tight, + }) { + // TODO(mdebbar): After layout, each paragraph span should have info about + // its position and dimensions. + // + // 1. Find the spans where the `start` and `end` indices fall. + // 2. If it's the same span, find the sub-box from `start` to `end`. + // 3. Else, find the trailing box(es) of the `start` span, and the `leading` + // box(es) of the `end` span. + // 4. Include the boxes of all the spans in between. + return []; + } + + @override + ui.TextPosition getPositionForOffset(ui.Offset offset) { + // TODO(mdebbar): After layout, each paragraph span should have info about + // its position and dimensions. Use that information to find which span the + // offset belongs to, then search within that span for the exact character. + return const ui.TextPosition(offset: 0); + } + + @override + ui.TextRange getWordBoundary(ui.TextPosition position) { + final String text = toPlainText(); + + final int start = WordBreaker.prevBreakIndex(text, position.offset + 1); + final int end = WordBreaker.nextBreakIndex(text, position.offset); + return ui.TextRange(start: start, end: end); + } + + @override + ui.TextRange getLineBoundary(ui.TextPosition position) { + // TODO(mdebbar): After layout, line metrics should be available and can be + // used to determine the line boundary of the given `position`. + return ui.TextRange.empty; + } + + @override + List computeLineMetrics() { + // TODO(mdebbar): After layout, line metrics should be available. + return []; + } +} + +/// A common interface for all types of spans that make up a paragraph. +/// +/// These spans are stored as a flat list in the paragraph object. +abstract class ParagraphSpan { + const ParagraphSpan(); +} + +/// Represent a span of text in the paragraph. +/// +/// It's a "flat" text span as opposed to the framework text spans that are +/// hierarchical. +/// +/// Instead of keeping spans and styles in a tree hierarchy like the framework +/// does, we flatten the structure and resolve/merge all the styles from parent +/// nodes. +class FlatTextSpan extends ParagraphSpan { + /// Creates a [FlatTextSpan] with the given [text] and [style]. + const FlatTextSpan({required this.text, required this.style}); + + /// The textual content of the span. + final String text; + + /// The resolved style of the span. + final EngineTextStyle style; +} + +/// Represents a node in the tree of text styles pushed to [ui.ParagraphBuilder]. +/// +/// The [ui.ParagraphBuilder.pushText] and [ui.ParagraphBuilder.pop] operations +/// represent the entire tree of styles in the paragraph. In our implementation, +/// we don't need to keep the entire tree structure in memory. At any point in +/// time, we only need a stack of nodes that represent the current branch in the +/// tree. The items in the stack are [StyleNode] objects. +abstract class StyleNode { + /// Create a child for this style node. + /// + /// We are not creating a tree structure, hence there's no need to keep track + /// of the children. + ChildStyleNode createChild(EngineTextStyle style) { + return ChildStyleNode(parent: this, style: style); + } + + /// Generates the final text style to be applied to the text span. + /// + /// The resolved text style is equivalent to the entire ascendent chain of + /// parent style nodes. + EngineTextStyle resolveStyle(); +} + +/// Represents a non-root [StyleNode]. +class ChildStyleNode extends StyleNode { + /// Creates a [ChildStyleNode] with the given [parent] and [style]. + ChildStyleNode({required this.parent, required this.style}); + + /// The parent node to be used when resolving text styles. + final StyleNode parent; + + /// The text style associated with the current node. + final EngineTextStyle style; + + @override + EngineTextStyle resolveStyle() { + // TODO(mdebbar): combine all styles from the parent hierarchy. + return style; + } +} + +/// The root style node for the paragraph. +/// +/// The style of the root is derived from a [ui.ParagraphStyle] and is the root +/// style for all spans in the paragraph. +class RootStyleNode extends StyleNode { + /// Creates a [RootStyleNode] from [paragraphStyle]. + RootStyleNode(this.paragraphStyle); + + /// The style of the paragraph being built. + final EngineParagraphStyle paragraphStyle; + + EngineTextStyle? _cachedStyle; + + @override + EngineTextStyle resolveStyle() { + final EngineTextStyle? style = _cachedStyle; + if (style == null) { + return _cachedStyle ??= + EngineTextStyle.fromParagraphStyle(paragraphStyle); + } + return style; + } +} + +/// Builds a [CanvasParagraph] containing text with the given styling +/// information. +class CanvasParagraphBuilder implements ui.ParagraphBuilder { + /// Creates a [CanvasParagraphBuilder] object, which is used to create a + /// [CanvasParagraph]. + CanvasParagraphBuilder(EngineParagraphStyle style) + : _paragraphStyle = style, + _rootStyleNode = RootStyleNode(style); + + final EngineParagraphStyle _paragraphStyle; + + final List _spans = []; + final List _styleStack = []; + + RootStyleNode _rootStyleNode; + StyleNode get _currentStyleNode => _styleStack.isEmpty + ? _rootStyleNode + : _styleStack[_styleStack.length - 1]; + + @override + int get placeholderCount => _placeholderCount; + int _placeholderCount = 0; + + @override + List get placeholderScales => _placeholderScales; + final List _placeholderScales = []; + + @override + void addPlaceholder( + double width, + double height, + ui.PlaceholderAlignment alignment, { + double scale = 1.0, + double? baselineOffset, + ui.TextBaseline? baseline, + }) { + // TODO(mdebbar): for measurement of placeholders, look at: + // - https://github.com/flutter/engine/blob/c0f7e8acf9318d264ad6a235facd097de597ffcc/third_party/txt/src/txt/paragraph_txt.cc#L325-L350 + + // Require a baseline to be specified if using a baseline-based alignment. + assert((alignment == ui.PlaceholderAlignment.aboveBaseline || + alignment == ui.PlaceholderAlignment.belowBaseline || + alignment == ui.PlaceholderAlignment.baseline) + ? baseline != null + : true); + + _placeholderCount++; + _placeholderScales.add(scale); + _spans.add(ParagraphPlaceholder( + width * scale, + height * scale, + alignment, + baselineOffset: (baselineOffset ?? height) * scale, + baseline: baseline ?? ui.TextBaseline.alphabetic, + )); + } + + @override + void pushStyle(ui.TextStyle style) { + _styleStack.add(_currentStyleNode.createChild(style as EngineTextStyle)); + } + + @override + void pop() { + if (_styleStack.isNotEmpty) { + _styleStack.removeLast(); + } + } + + @override + void addText(String text) { + _spans + .add(FlatTextSpan(text: text, style: _currentStyleNode.resolveStyle())); + } + + @override + CanvasParagraph build() { + return CanvasParagraph( + _spans, + paragraphStyle: _paragraphStyle, + placeholderCount: _placeholderCount, + ); + } +} diff --git a/lib/web_ui/lib/src/engine/text/measurement.dart b/lib/web_ui/lib/src/engine/text/measurement.dart index 416f045603b07..426e267967751 100644 --- a/lib/web_ui/lib/src/engine/text/measurement.dart +++ b/lib/web_ui/lib/src/engine/text/measurement.dart @@ -202,7 +202,7 @@ abstract class TextMeasurementService { // see: https://github.com/flutter/flutter/issues/36341 if (!window.physicalSize.isEmpty && WebExperiments.instance!.useCanvasText && - _canUseCanvasMeasurement(paragraph as EngineParagraph)) { + _canUseCanvasMeasurement(paragraph as DomParagraph)) { return canvasInstance; } return domInstance; @@ -214,7 +214,7 @@ abstract class TextMeasurementService { rulerManager?._evictAllRulers(); } - static bool _canUseCanvasMeasurement(EngineParagraph paragraph) { + static bool _canUseCanvasMeasurement(DomParagraph paragraph) { // Currently, the canvas-based approach only works on plain text that // doesn't have any of the following styles: // - decoration @@ -227,7 +227,7 @@ abstract class TextMeasurementService { /// Measures the paragraph and returns a [MeasurementResult] object. MeasurementResult? measure( - EngineParagraph paragraph, + DomParagraph paragraph, ui.ParagraphConstraints constraints, ) { assert(rulerManager != null); @@ -255,16 +255,16 @@ abstract class TextMeasurementService { /// Measures the width of a substring of the given [paragraph] with no /// constraints. - double measureSubstringWidth(EngineParagraph paragraph, int start, int end); + double measureSubstringWidth(DomParagraph paragraph, int start, int end); /// Returns text position given a paragraph, constraints and offset. - ui.TextPosition getTextPositionForOffset(EngineParagraph paragraph, + ui.TextPosition getTextPositionForOffset(DomParagraph paragraph, ui.ParagraphConstraints? constraints, ui.Offset offset); /// Delegates to a [ParagraphRuler] to measure a list of text boxes that /// enclose the given range of text. List measureBoxesForRange( - EngineParagraph paragraph, + DomParagraph paragraph, ui.ParagraphConstraints constraints, { required int start, required int end, @@ -302,7 +302,7 @@ abstract class TextMeasurementService { /// paragraph. When that's available, it can be used by a canvas to render /// the text line. MeasurementResult _doMeasure( - EngineParagraph paragraph, + DomParagraph paragraph, ui.ParagraphConstraints constraints, ParagraphRuler ruler, ); @@ -325,7 +325,7 @@ class DomTextMeasurementService extends TextMeasurementService { @override MeasurementResult _doMeasure( - EngineParagraph paragraph, + DomParagraph paragraph, ui.ParagraphConstraints constraints, ParagraphRuler ruler, ) { @@ -350,7 +350,7 @@ class DomTextMeasurementService extends TextMeasurementService { } @override - double measureSubstringWidth(EngineParagraph paragraph, int start, int end) { + double measureSubstringWidth(DomParagraph paragraph, int start, int end) { assert(paragraph._plainText != null); final ParagraphGeometricStyle style = paragraph._geometricStyle; final ParagraphRuler ruler = @@ -359,7 +359,7 @@ class DomTextMeasurementService extends TextMeasurementService { final String text = paragraph._plainText!.substring(start, end); final ui.Paragraph substringParagraph = paragraph._cloneWithText(text); - ruler.willMeasure(substringParagraph as EngineParagraph); + ruler.willMeasure(substringParagraph as DomParagraph); ruler.measureAsSingleLine(); final TextDimensions dimensions = ruler.singleLineDimensions; ruler.didMeasure(); @@ -367,7 +367,7 @@ class DomTextMeasurementService extends TextMeasurementService { } @override - ui.TextPosition getTextPositionForOffset(EngineParagraph paragraph, + ui.TextPosition getTextPositionForOffset(DomParagraph paragraph, ui.ParagraphConstraints? constraints, ui.Offset offset) { assert( paragraph._measurementResult!.lines == null, @@ -398,7 +398,7 @@ class DomTextMeasurementService extends TextMeasurementService { /// This method still needs to measure `minIntrinsicWidth`. MeasurementResult _measureSingleLineParagraph( ParagraphRuler ruler, - EngineParagraph paragraph, + DomParagraph paragraph, ui.ParagraphConstraints constraints, ) { final double width = constraints.width; @@ -463,7 +463,7 @@ class DomTextMeasurementService extends TextMeasurementService { /// and get new values for width, height and alphabetic baseline. We also need /// to measure `minIntrinsicWidth`. MeasurementResult _measureMultiLineParagraph(ParagraphRuler ruler, - EngineParagraph paragraph, ui.ParagraphConstraints constraints) { + DomParagraph paragraph, ui.ParagraphConstraints constraints) { // If constraint is infinite, we must use _measureSingleLineParagraph final double width = constraints.width; final double minIntrinsicWidth = ruler.minIntrinsicDimensions.width; @@ -549,7 +549,7 @@ class CanvasTextMeasurementService extends TextMeasurementService { @override MeasurementResult _doMeasure( - EngineParagraph paragraph, + DomParagraph paragraph, ui.ParagraphConstraints constraints, ParagraphRuler ruler, ) { @@ -619,7 +619,7 @@ class CanvasTextMeasurementService extends TextMeasurementService { } @override - double measureSubstringWidth(EngineParagraph paragraph, int start, int end) { + double measureSubstringWidth(DomParagraph paragraph, int start, int end) { assert(paragraph._plainText != null); final String text = paragraph._plainText!; final ParagraphGeometricStyle style = paragraph._geometricStyle; @@ -725,7 +725,7 @@ class LinesCalculator { LinesCalculator(this._canvasContext, this._paragraph, this._maxWidth); final html.CanvasRenderingContext2D _canvasContext; - final EngineParagraph _paragraph; + final DomParagraph _paragraph; final double _maxWidth; String? get _text => _paragraph._plainText; @@ -997,7 +997,7 @@ class MaxIntrinsicCalculator { /// Calculates the offset necessary for the given line to be correctly aligned. double _calculateAlignOffsetForLine({ - required EngineParagraph paragraph, + required DomParagraph paragraph, required double lineWidth, required double maxWidth, }) { diff --git a/lib/web_ui/lib/src/engine/text/paragraph.dart b/lib/web_ui/lib/src/engine/text/paragraph.dart index 7468c22fe5c4f..adf8e1f5acb70 100644 --- a/lib/web_ui/lib/src/engine/text/paragraph.dart +++ b/lib/web_ui/lib/src/engine/text/paragraph.dart @@ -162,13 +162,42 @@ class EngineLineMetrics implements ui.LineMetrics { } } -/// The web implementation of [ui.Paragraph]. -class EngineParagraph implements ui.Paragraph { +/// Common interface for all the implementations of [ui.Paragraph] in the web +/// engine. +abstract class EngineParagraph implements ui.Paragraph { + /// Whether this paragraph has been laid out or not. + bool get isLaidOut; + + /// Whether this paragraph can be drawn on a bitmap canvas. + bool get drawOnCanvas; + + /// Whether this paragraph is doing arbitrary paint operations that require + /// a bitmap canvas, and can't be expressed in a DOM canvas. + bool get hasArbitraryPaint; + + void paint(BitmapCanvas canvas, ui.Offset offset); + + /// Generates a flat string computed from all the spans of the paragraph. + String toPlainText(); + + /// Returns a DOM element that represents the entire paragraph and its + /// children. + /// + /// Generates a new DOM element on every invokation. + html.HtmlElement toDomElement(); +} + +/// Uses the DOM and hierarchical elements to represent the span of the +/// paragraph. +/// +/// This implementation will go away once the new [CanvasParagraph] is +/// complete and turned on by default. +class DomParagraph implements EngineParagraph { /// This class is created by the engine, and should not be instantiated /// or extended directly. /// - /// To create a [ui.Paragraph] object, use a [ui.ParagraphBuilder]. - EngineParagraph({ + /// To create a [DomParagraph] object, use a [DomParagraphBuilder]. + DomParagraph({ required html.HtmlElement paragraphElement, required ParagraphGeometricStyle geometricStyle, required String? plainText, @@ -337,6 +366,7 @@ class EngineParagraph implements ui.Paragraph { bool get hasArbitraryPaint => _geometricStyle.ellipsis != null; + @override void paint(BitmapCanvas canvas, ui.Offset offset) { assert(drawOnCanvas); assert(isLaidOut); @@ -395,6 +425,13 @@ class EngineParagraph implements ui.Paragraph { } } + @override + String toPlainText() { + return _plainText ?? + js_util.getProperty(_paragraphElement, 'textContent') as String; + } + + @override html.HtmlElement toDomElement() { assert(isLaidOut); @@ -565,7 +602,7 @@ class EngineParagraph implements ui.Paragraph { } ui.Paragraph _cloneWithText(String plainText) { - return EngineParagraph( + return DomParagraph( plainText: plainText, paragraphElement: _paragraphElement.clone(true) as html.HtmlElement, geometricStyle: _geometricStyle, @@ -902,6 +939,15 @@ class EngineTextStyle implements ui.TextStyle { _foreground = foreground, _shadows = shadows; + factory EngineTextStyle.fromParagraphStyle(EngineParagraphStyle paragraphStyle) => EngineTextStyle( + fontWeight: paragraphStyle._fontWeight, + fontStyle: paragraphStyle._fontStyle, + fontFamily: paragraphStyle._fontFamily, + fontSize: paragraphStyle._fontSize, + height: paragraphStyle._height, + locale: paragraphStyle._locale, + ); + final ui.Color? _color; final ui.TextDecoration? _decoration; final ui.Color? _decorationColor; @@ -1114,7 +1160,7 @@ class EngineStrutStyle implements ui.StrutStyle { } /// The web implementation of [ui.ParagraphBuilder]. -class EngineParagraphBuilder implements ui.ParagraphBuilder { +class DomParagraphBuilder implements ui.ParagraphBuilder { /// Marks a call to the [pop] method in the [_ops] list. static final Object _paragraphBuilderPop = Object(); @@ -1122,9 +1168,9 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder { final EngineParagraphStyle _paragraphStyle; final List _ops = []; - /// Creates an [EngineParagraphBuilder] object, which is used to create a - /// [EngineParagraph]. - EngineParagraphBuilder(EngineParagraphStyle style) : _paragraphStyle = style { + /// Creates a [DomParagraphBuilder] object, which is used to create a + /// [DomParagraph]. + DomParagraphBuilder(EngineParagraphStyle style) : _paragraphStyle = style { // TODO(b/128317744): Implement support for strut font families. List strutFontFamilies; if (style._strutStyle != null) { @@ -1337,7 +1383,7 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder { // Empty paragraph. _applyTextStyleToElement( element: _paragraphElement, style: cumulativeStyle); - return EngineParagraph( + return DomParagraph( paragraphElement: _paragraphElement, geometricStyle: ParagraphGeometricStyle( textDirection: _paragraphStyle._effectiveTextDirection, @@ -1394,7 +1440,7 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder { _applyTextBackgroundToElement( element: _paragraphElement, style: cumulativeStyle); } - return EngineParagraph( + return DomParagraph( paragraphElement: _paragraphElement, geometricStyle: ParagraphGeometricStyle( textDirection: _paragraphStyle._effectiveTextDirection, @@ -1450,7 +1496,7 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder { } } - return EngineParagraph( + return DomParagraph( paragraphElement: _paragraphElement, geometricStyle: ParagraphGeometricStyle( textDirection: _paragraphStyle._effectiveTextDirection, @@ -1476,7 +1522,7 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder { /// Holds information for a placeholder in a paragraph. /// /// [width], [height] and [baselineOffset] are expected to be already scaled. -class ParagraphPlaceholder { +class ParagraphPlaceholder extends ParagraphSpan { ParagraphPlaceholder( this.width, this.height, diff --git a/lib/web_ui/lib/src/engine/text/ruler.dart b/lib/web_ui/lib/src/engine/text/ruler.dart index e649090c19aa7..cfd6d2c6ae90d 100644 --- a/lib/web_ui/lib/src/engine/text/ruler.dart +++ b/lib/web_ui/lib/src/engine/text/ruler.dart @@ -178,7 +178,7 @@ class TextDimensions { /// /// The primary efficiency gain is from rare occurrence of rich text in /// typical apps. - void updateText(EngineParagraph from, ParagraphGeometricStyle style) { + void updateText(DomParagraph from, ParagraphGeometricStyle style) { assert(from != null); // ignore: unnecessary_null_comparison assert(_element != null); // ignore: unnecessary_null_comparison assert(from._debugHasSameRootStyle(style)); @@ -563,12 +563,12 @@ class ParagraphRuler { } /// The paragraph being measured. - EngineParagraph? _paragraph; + DomParagraph? _paragraph; /// Prepares this ruler for measuring the given [paragraph]. /// /// This method must be called before calling any of the `measure*` methods. - void willMeasure(EngineParagraph paragraph) { + void willMeasure(DomParagraph paragraph) { assert(paragraph != null); // ignore: unnecessary_null_comparison assert(() { if (_paragraph != null) { @@ -855,7 +855,7 @@ class ParagraphRuler { // is changing. static const int _constraintCacheSize = 8; - void cacheMeasurement(EngineParagraph paragraph, MeasurementResult? item) { + void cacheMeasurement(DomParagraph paragraph, MeasurementResult? item) { final String? plainText = paragraph._plainText; final List constraintCache = _measurementCache[plainText] ??= []; @@ -874,7 +874,7 @@ class ParagraphRuler { } MeasurementResult? cacheLookup( - EngineParagraph paragraph, ui.ParagraphConstraints constraints) { + DomParagraph paragraph, ui.ParagraphConstraints constraints) { final String? plainText = paragraph._plainText; if (plainText == null) { // Multi span paragraph, do not use cache item. diff --git a/lib/web_ui/lib/src/ui/text.dart b/lib/web_ui/lib/src/ui/text.dart index f5fd2f05a8757..fbf9321813116 100644 --- a/lib/web_ui/lib/src/ui/text.dart +++ b/lib/web_ui/lib/src/ui/text.dart @@ -588,7 +588,7 @@ abstract class ParagraphBuilder { if (engine.useCanvasKit) { return engine.CkParagraphBuilder(style); } else { - return engine.EngineParagraphBuilder(style as engine.EngineParagraphStyle); + return engine.DomParagraphBuilder(style as engine.EngineParagraphStyle); } } void pushStyle(TextStyle style); diff --git a/lib/web_ui/test/text/canvas_paragraph_builder_test.dart b/lib/web_ui/test/text/canvas_paragraph_builder_test.dart new file mode 100644 index 0000000000000..e13078c0fbd5c --- /dev/null +++ b/lib/web_ui/test/text/canvas_paragraph_builder_test.dart @@ -0,0 +1,156 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.10 +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + setUpAll(() { + WebExperiments.ensureInitialized(); + }); + + test('Builds a text-only canvas paragraph', () { + final EngineParagraphStyle style = EngineParagraphStyle(fontSize: 13.0); + final CanvasParagraphBuilder builder = CanvasParagraphBuilder(style); + + builder.addText('Hello'); + + final CanvasParagraph paragraph = builder.build(); + expect(paragraph.paragraphStyle, style); + expect(paragraph.toPlainText(), 'Hello'); + expect(paragraph.spans, hasLength(1)); + + final ParagraphSpan span = paragraph.spans.single; + expect(span, isA()); + final FlatTextSpan textSpan = span as FlatTextSpan; + expect(textSpan.text, 'Hello'); + expect(textSpan.style, EngineTextStyle(fontSize: 13.0)); + }); + + test('Builds a single-span paragraph with complex styles', () { + final EngineParagraphStyle style = + EngineParagraphStyle(fontSize: 13.0, height: 1.5); + final CanvasParagraphBuilder builder = CanvasParagraphBuilder(style); + + builder.pushStyle(TextStyle(fontSize: 9.0)); + builder.pushStyle(TextStyle(fontWeight: FontWeight.bold)); + builder.pushStyle(TextStyle(fontSize: 40.0)); + builder.pop(); + builder + .pushStyle(TextStyle(fontStyle: FontStyle.italic, letterSpacing: 2.0)); + builder.addText('Hello'); + + final CanvasParagraph paragraph = builder.build(); + expect(paragraph.toPlainText(), 'Hello'); + expect(paragraph.spans, hasLength(1)); + + final FlatTextSpan span = paragraph.spans.single as FlatTextSpan; + expect(span.text, 'Hello'); + // TODO(mdebbar): Uncomment this once style resolution is implemented. + // expect( + // span.style, + // TextStyle( + // height: 1.5, + // fontSize: 9.0, + // fontWeight: FontWeight.bold, + // fontStyle: FontStyle.italic, + // letterSpacing: 2.0, + // ), + // ); + }); + + test('Builds a multi-span paragraph', () { + final EngineParagraphStyle style = EngineParagraphStyle(fontSize: 13.0); + final CanvasParagraphBuilder builder = CanvasParagraphBuilder(style); + + builder.pushStyle(TextStyle(fontWeight: FontWeight.bold)); + builder.addText('Hello'); + builder.pop(); + builder.pushStyle(TextStyle(fontStyle: FontStyle.italic)); + builder.addText(' world'); + + final CanvasParagraph paragraph = builder.build(); + expect(paragraph.toPlainText(), 'Hello world'); + expect(paragraph.spans, hasLength(2)); + + final FlatTextSpan hello = paragraph.spans.first as FlatTextSpan; + expect(hello.text, 'Hello'); + // TODO(mdebbar): Uncomment this once style resolution is implemented. + // expect( + // hello.style, + // TextStyle( + // fontSize: 13.0, + // fontWeight: FontWeight.bold, + // ), + // ); + + final FlatTextSpan world = paragraph.spans.last as FlatTextSpan; + expect(world.text, ' world'); + // TODO(mdebbar): Uncomment this once style resolution is implemented. + // expect( + // world.style, + // TextStyle( + // fontSize: 13.0, + // fontStyle: FontStyle.italic, + // ), + // ); + }); + + test('Builds a multi-span paragraph with complex styles', () { + final EngineParagraphStyle style = EngineParagraphStyle(fontSize: 13.0); + final CanvasParagraphBuilder builder = CanvasParagraphBuilder(style); + + builder.pushStyle(TextStyle(fontWeight: FontWeight.bold)); + builder.pushStyle(TextStyle(height: 2.0)); + builder.addText('Hello'); + builder.pop(); // pop TextStyle(height: 2.0). + builder.pushStyle(TextStyle(fontStyle: FontStyle.italic)); + builder.addText(' world'); + builder.pushStyle(TextStyle(fontWeight: FontWeight.normal)); + builder.addText('!'); + + final CanvasParagraph paragraph = builder.build(); + expect(paragraph.toPlainText(), 'Hello world!'); + expect(paragraph.spans, hasLength(3)); + + final FlatTextSpan hello = paragraph.spans[0] as FlatTextSpan; + expect(hello.text, 'Hello'); + // TODO(mdebbar): Uncomment this once style resolution is implemented. + // expect( + // hello.style, + // TextStyle(fontSize: 13.0, fontWeight: FontWeight.bold, height: 2.0), + // ); + + final FlatTextSpan world = paragraph.spans[1] as FlatTextSpan; + expect(world.text, ' world'); + // TODO(mdebbar): Uncomment this once style resolution is implemented. + // expect( + // world.style, + // TextStyle( + // fontSize: 13.0, + // fontWeight: FontWeight.bold, + // fontStyle: FontStyle.italic, + // ), + // ); + + final FlatTextSpan bang = paragraph.spans[2] as FlatTextSpan; + expect(bang.text, '!'); + // TODO(mdebbar): Uncomment this once style resolution is implemented. + // expect( + // bang.style, + // TextStyle( + // fontSize: 13.0, + // fontWeight: FontWeight.normal, + // fontStyle: FontStyle.italic, + // ), + // ); + }); +} From 62275a0878054ef478c28c7ad0c98e5e29562f22 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Wed, 4 Nov 2020 16:29:21 -0800 Subject: [PATCH 2/3] fix licenses + fix types in some tests --- ci/licenses_golden/licenses_flutter | 1 + lib/web_ui/lib/src/engine/text/paragraph.dart | 2 +- lib/web_ui/test/text/measurement_test.dart | 6 +++--- lib/web_ui/test/text_test.dart | 18 +++++++++--------- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 245980e77e921..93948761cd3b5 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -526,6 +526,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/services/message_codecs.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/services/serialization.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/shadow.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/test_embedding.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/font_collection.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/line_break_properties.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/line_breaker.dart diff --git a/lib/web_ui/lib/src/engine/text/paragraph.dart b/lib/web_ui/lib/src/engine/text/paragraph.dart index adf8e1f5acb70..8c804688dfa37 100644 --- a/lib/web_ui/lib/src/engine/text/paragraph.dart +++ b/lib/web_ui/lib/src/engine/text/paragraph.dart @@ -183,7 +183,7 @@ abstract class EngineParagraph implements ui.Paragraph { /// Returns a DOM element that represents the entire paragraph and its /// children. /// - /// Generates a new DOM element on every invokation. + /// Generates a new DOM element on every invocation. html.HtmlElement toDomElement(); } diff --git a/lib/web_ui/test/text/measurement_test.dart b/lib/web_ui/test/text/measurement_test.dart index ca4258c89cc77..2b5c92571a116 100644 --- a/lib/web_ui/test/text/measurement_test.dart +++ b/lib/web_ui/test/text/measurement_test.dart @@ -64,8 +64,8 @@ void testMain() async { final ui.ParagraphStyle s3 = ui.ParagraphStyle(fontSize: 22.0); ParagraphGeometricStyle style1, style2, style3; - EngineParagraph style1Text1, style1Text2; // two paragraphs sharing style - EngineParagraph style2Text1, style3Text3; + DomParagraph style1Text1, style1Text2; // two paragraphs sharing style + DomParagraph style2Text1, style3Text3; setUp(() { style1Text1 = build(s1, '1'); @@ -481,7 +481,7 @@ void testMain() async { testMeasurements( 'wraps multi-line text correctly when constraint width is infinite', (TextMeasurementService instance) { - final EngineParagraph paragraph = build(ahemStyle, '123\n456 789'); + final DomParagraph paragraph = build(ahemStyle, '123\n456 789'); final MeasurementResult result = instance.measure(paragraph, infiniteConstraints); diff --git a/lib/web_ui/test/text_test.dart b/lib/web_ui/test/text_test.dart index 35861b2eda6c0..4daf63e843ed6 100644 --- a/lib/web_ui/test/text_test.dart +++ b/lib/web_ui/test/text_test.dart @@ -96,7 +96,7 @@ void testMain() async { fontSize: 14.0, )); builder.addText('How do you do this fine morning?'); - final EngineParagraph paragraph = builder.build(); + final DomParagraph paragraph = builder.build(); expect(paragraph.paragraphElement.parent, isNull); expect(paragraph.height, 0.0); @@ -162,7 +162,7 @@ void testMain() async { fontSize: 15.0, )); builder.addText('hi'); - EngineParagraph paragraph = builder.build(); + DomParagraph paragraph = builder.build(); expect(paragraph.plainText, isNotNull); expect(paragraph.geometricStyle.fontWeight, FontWeight.normal); @@ -189,7 +189,7 @@ void testMain() async { builder.addText('h'); builder.pushStyle(TextStyle(fontWeight: FontWeight.bold)); builder.addText('i'); - final EngineParagraph paragraph = builder.build(); + final DomParagraph paragraph = builder.build(); expect(paragraph.plainText, isNull); expect(paragraph.geometricStyle.fontWeight, FontWeight.normal); }); @@ -202,7 +202,7 @@ void testMain() async { fontSize: 15.0, )); builder.pushStyle(TextStyle(fontWeight: FontWeight.bold)); - final EngineParagraph paragraph = builder.build(); + final DomParagraph paragraph = builder.build(); expect(paragraph.plainText, ''); expect(paragraph.geometricStyle.fontWeight, FontWeight.bold); }); @@ -223,7 +223,7 @@ void testMain() async { builder.addText(secondSpanText); builder.pushStyle(TextStyle(fontStyle: FontStyle.italic)); builder.addText('followed by a link'); - final EngineParagraph paragraph = builder.build(); + final DomParagraph paragraph = builder.build(); paragraph.layout(const ParagraphConstraints(width: 800.0)); expect(paragraph.plainText, isNull); const int secondSpanStartPosition = firstSpanText.length; @@ -371,7 +371,7 @@ void testMain() async { builder.pushStyle(TextStyle(fontSize: 30.0, fontWeight: FontWeight.normal)); const String secondSpanText = 'def'; builder.addText(secondSpanText); - final EngineParagraph paragraph = builder.build(); + final DomParagraph paragraph = builder.build(); paragraph.layout(const ParagraphConstraints(width: 800.0)); expect(paragraph.plainText, isNull); final List spans = @@ -396,7 +396,7 @@ void testMain() async { builder.addText('Hello'); - final EngineParagraph paragraph = builder.build(); + final DomParagraph paragraph = builder.build(); expect(paragraph.paragraphElement.style.fontFamily, 'SomeFont, $fallback, sans-serif'); @@ -418,7 +418,7 @@ void testMain() async { builder.addText('Hello'); - final EngineParagraph paragraph = builder.build(); + final DomParagraph paragraph = builder.build(); expect(paragraph.paragraphElement.style.fontFamily, 'serif'); debugEmulateFlutterTesterEnvironment = true; @@ -435,7 +435,7 @@ void testMain() async { builder.addText('Hello'); - final EngineParagraph paragraph = builder.build(); + final DomParagraph paragraph = builder.build(); expect(paragraph.paragraphElement.style.fontFamily, '"MyFont 2000", $fallback, sans-serif'); From d46a52ed667f7400f5b4a0d8c4f3ca89d39c4959 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Fri, 6 Nov 2020 10:24:33 -0800 Subject: [PATCH 3/3] address comments --- lib/web_ui/lib/src/engine/text/canvas_paragraph.dart | 1 + lib/web_ui/lib/src/engine/text/paragraph.dart | 1 + lib/web_ui/test/text/canvas_paragraph_builder_test.dart | 3 +++ 3 files changed, 5 insertions(+) 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 5f7d54c895567..a0c8eaefd862d 100644 --- a/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart +++ b/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart @@ -30,6 +30,7 @@ class CanvasParagraph implements EngineParagraph { /// The number of placeholders in this paragraph. final int placeholderCount; + // Defaulting to -1 for non-laid-out paragraphs like the native engine does. @override double width = -1.0; diff --git a/lib/web_ui/lib/src/engine/text/paragraph.dart b/lib/web_ui/lib/src/engine/text/paragraph.dart index 8c804688dfa37..ec3a59bcfb106 100644 --- a/lib/web_ui/lib/src/engine/text/paragraph.dart +++ b/lib/web_ui/lib/src/engine/text/paragraph.dart @@ -245,6 +245,7 @@ class DomParagraph implements EngineParagraph { bool get _hasLineMetrics => _measurementResult?.lines != null; + // Defaulting to -1 for non-laid-out paragraphs like the native engine does. @override double get width => _measurementResult?.width ?? -1; diff --git a/lib/web_ui/test/text/canvas_paragraph_builder_test.dart b/lib/web_ui/test/text/canvas_paragraph_builder_test.dart index e13078c0fbd5c..c61ae96fcb89f 100644 --- a/lib/web_ui/test/text/canvas_paragraph_builder_test.dart +++ b/lib/web_ui/test/text/canvas_paragraph_builder_test.dart @@ -17,6 +17,9 @@ void testMain() { WebExperiments.ensureInitialized(); }); + // TODO(mdebbar): Add checks for the output of `toDomElement()` in all the + // tests below. + test('Builds a text-only canvas paragraph', () { final EngineParagraphStyle style = EngineParagraphStyle(fontSize: 13.0); final CanvasParagraphBuilder builder = CanvasParagraphBuilder(style);