• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5part of engine;
6
7/// The web implementation of [ui.Paragraph].
8class EngineParagraph implements ui.Paragraph {
9  /// This class is created by the engine, and should not be instantiated
10  /// or extended directly.
11  ///
12  /// To create a [ui.Paragraph] object, use a [ui.ParagraphBuilder].
13  EngineParagraph({
14    @required html.HtmlElement paragraphElement,
15    @required ParagraphGeometricStyle geometricStyle,
16    @required String plainText,
17    @required ui.Paint paint,
18    @required ui.TextAlign textAlign,
19    @required ui.TextDirection textDirection,
20    @required ui.Paint background,
21  })  : assert((plainText == null && paint == null) ||
22            (plainText != null && paint != null)),
23        _paragraphElement = paragraphElement,
24        _geometricStyle = geometricStyle,
25        _plainText = plainText,
26        _textAlign = textAlign,
27        _textDirection = textDirection,
28        _paint = paint,
29        _background = background;
30
31  final html.HtmlElement _paragraphElement;
32  final ParagraphGeometricStyle _geometricStyle;
33  final String _plainText;
34  final ui.Paint _paint;
35  final ui.TextAlign _textAlign;
36  final ui.TextDirection _textDirection;
37  final ui.Paint _background;
38
39  @visibleForTesting
40  String get plainText => _plainText;
41
42  @visibleForTesting
43  html.HtmlElement get paragraphElement => _paragraphElement;
44
45  @visibleForTesting
46  ParagraphGeometricStyle get geometricStyle => _geometricStyle;
47
48  /// The instance of [TextMeasurementService] to be used to measure this
49  /// paragraph.
50  TextMeasurementService get _measurementService =>
51      TextMeasurementService.forParagraph(this);
52
53  /// The measurement result of the last layout operation.
54  MeasurementResult _measurementResult;
55
56  @override
57  double get width => _measurementResult?.width ?? -1;
58
59  @override
60  double get height => _measurementResult?.height ?? 0;
61
62  /// {@template dart.ui.paragraph.naturalHeight}
63  /// The amount of vertical space the paragraph occupies while ignoring the
64  /// [ParagraphGeometricStyle.maxLines] constraint.
65  /// {@endtemplate}
66  ///
67  /// Valid only after [layout] has been called.
68  double get _naturalHeight => _measurementResult?.naturalHeight ?? 0;
69
70  /// The amount of vertical space one line of this paragraph occupies.
71  ///
72  /// Valid only after [layout] has been called.
73  double get _lineHeight => _measurementResult?.lineHeight ?? 0;
74
75  // TODO(flutter_web): see https://github.com/flutter/flutter/issues/33613.
76  @override
77  double get longestLine => 0;
78
79  @override
80  double get minIntrinsicWidth => _measurementResult?.minIntrinsicWidth ?? 0;
81
82  @override
83  double get maxIntrinsicWidth => _measurementResult?.maxIntrinsicWidth ?? 0;
84
85  @override
86  double get alphabeticBaseline => _measurementResult?.alphabeticBaseline ?? -1;
87
88  @override
89  double get ideographicBaseline =>
90      _measurementResult?.ideographicBaseline ?? -1;
91
92  @override
93  bool get didExceedMaxLines => _didExceedMaxLines;
94  bool _didExceedMaxLines = false;
95
96  ui.ParagraphConstraints _lastUsedConstraints;
97
98  /// Returns horizontal alignment offset for single line text when rendering
99  /// directly into a canvas without css text alignment styling.
100  double _alignOffset = 0.0;
101
102  /// If not null, this list would contain the strings representing each line
103  /// in the paragraph.
104  List<String> get _lines => _measurementResult?.lines;
105
106  @override
107  void layout(ui.ParagraphConstraints constraints) {
108    if (constraints == _lastUsedConstraints) {
109      return;
110    }
111
112    _measurementResult = _measurementService.measure(this, constraints);
113    _lastUsedConstraints = constraints;
114
115    if (_geometricStyle.maxLines != null) {
116      _didExceedMaxLines = _naturalHeight > height;
117    } else {
118      _didExceedMaxLines = false;
119    }
120
121    if (_measurementResult.isSingleLine && constraints != null) {
122      switch (_textAlign) {
123        case ui.TextAlign.center:
124          _alignOffset = (constraints.width - maxIntrinsicWidth) / 2.0;
125          break;
126        case ui.TextAlign.right:
127          _alignOffset = constraints.width - maxIntrinsicWidth;
128          break;
129        case ui.TextAlign.start:
130          _alignOffset = _textDirection == ui.TextDirection.rtl
131              ? constraints.width - maxIntrinsicWidth
132              : 0.0;
133          break;
134        case ui.TextAlign.end:
135          _alignOffset = _textDirection == ui.TextDirection.ltr
136              ? constraints.width - maxIntrinsicWidth
137              : 0.0;
138          break;
139        default:
140          _alignOffset = 0.0;
141          break;
142      }
143    }
144  }
145
146  @override
147  List<ui.TextBox> getBoxesForPlaceholders() {
148    return const <ui.TextBox>[];
149  }
150
151  /// Returns `true` if this paragraph can be directly painted to the canvas.
152  ///
153  ///
154  /// Examples of paragraphs that can't be drawn directly on the canvas:
155  ///
156  /// - Rich text where there are multiple pieces of text that have different
157  ///   styles.
158  /// - Paragraphs that contain decorations.
159  /// - Paragraphs that have a non-null word-spacing.
160  /// - Paragraphs with a background.
161  bool get _drawOnCanvas {
162    bool canDrawTextOnCanvas;
163    if (TextMeasurementService.enableExperimentalCanvasImplementation) {
164      canDrawTextOnCanvas = _lines != null;
165    } else {
166      canDrawTextOnCanvas = _measurementResult.isSingleLine &&
167          _plainText != null &&
168          _geometricStyle.ellipsis == null;
169    }
170
171    return canDrawTextOnCanvas &&
172        _geometricStyle.decoration == null &&
173        _geometricStyle.wordSpacing == null;
174  }
175
176  /// Whether this paragraph has been laid out.
177  bool get _isLaidOut => _measurementResult != null;
178
179  /// Asserts that the properties used to measure paragraph layout are the same
180  /// as the properties of this paragraphs root style.
181  ///
182  /// Ignores properties that do not affect layout, such as
183  /// [ParagraphStyle.textAlign].
184  bool _debugHasSameRootStyle(ParagraphGeometricStyle style) {
185    assert(() {
186      if (style != _geometricStyle) {
187        throw Exception('Attempted to measure a paragraph whose style is '
188            'different from the style of the ruler used to measure it.');
189      }
190      return true;
191    }());
192    return true;
193  }
194
195  @override
196  List<ui.TextBox> getBoxesForRange(
197    int start,
198    int end, {
199    ui.BoxHeightStyle boxHeightStyle = ui.BoxHeightStyle.tight,
200    ui.BoxWidthStyle boxWidthStyle = ui.BoxWidthStyle.tight,
201  }) {
202    assert(boxHeightStyle != null);
203    assert(boxWidthStyle != null);
204    if (_plainText == null) {
205      return <ui.TextBox>[];
206    }
207
208    final int length = _plainText.length;
209    // Ranges that are out of bounds should return an empty list.
210    if (start < 0 || end < 0 || start > length || end > length) {
211      return <ui.TextBox>[];
212    }
213
214    return _measurementService.measureBoxesForRange(
215      this,
216      _lastUsedConstraints,
217      start: start,
218      end: end,
219      alignOffset: _alignOffset,
220      textDirection: _textDirection,
221    );
222  }
223
224  ui.Paragraph _cloneWithText(String plainText) {
225    return EngineParagraph(
226      plainText: plainText,
227      paragraphElement: _paragraphElement.clone(true),
228      geometricStyle: _geometricStyle,
229      paint: _paint,
230      textAlign: _textAlign,
231      textDirection: _textDirection,
232      background: _background,
233    );
234  }
235
236  @override
237  ui.TextPosition getPositionForOffset(ui.Offset offset) {
238    if (_plainText == null) {
239      return const ui.TextPosition(offset: 0);
240    }
241
242    final double dx = offset.dx - _alignOffset;
243    final TextMeasurementService instance = _measurementService;
244
245    int low = 0;
246    int high = _plainText.length;
247    do {
248      final int current = (low + high) ~/ 2;
249      final double width = instance.measureSubstringWidth(this, 0, current);
250      if (width < dx) {
251        low = current;
252      } else if (width > dx) {
253        high = current;
254      } else {
255        low = high = current;
256      }
257    } while (high - low > 1);
258
259    if (low == high) {
260      // The offset falls exactly in between the two letters.
261      return ui.TextPosition(offset: high, affinity: ui.TextAffinity.upstream);
262    }
263
264    final double lowWidth = instance.measureSubstringWidth(this, 0, low);
265    final double highWidth = instance.measureSubstringWidth(this, 0, high);
266
267    if (dx - lowWidth < highWidth - dx) {
268      // The offset is closer to the low index.
269      return ui.TextPosition(offset: low, affinity: ui.TextAffinity.downstream);
270    } else {
271      // The offset is closer to high index.
272      return ui.TextPosition(offset: high, affinity: ui.TextAffinity.upstream);
273    }
274  }
275
276  @override
277  List<int> getWordBoundary(int offset) {
278    if (_plainText == null) {
279      return <int>[offset, offset];
280    }
281
282    final int start = WordBreaker.prevBreakIndex(_plainText, offset);
283    final int end = WordBreaker.nextBreakIndex(_plainText, offset);
284    return <int>[start, end];
285  }
286}
287
288/// The web implementation of [ui.ParagraphStyle].
289class EngineParagraphStyle implements ui.ParagraphStyle {
290  /// Creates a new instance of [EngineParagraphStyle].
291  EngineParagraphStyle({
292    ui.TextAlign textAlign,
293    ui.TextDirection textDirection,
294    int maxLines,
295    String fontFamily,
296    double fontSize,
297    double height,
298    ui.FontWeight fontWeight,
299    ui.FontStyle fontStyle,
300    ui.StrutStyle strutStyle,
301    String ellipsis,
302    ui.Locale locale,
303  })  : _textAlign = textAlign,
304        _textDirection = textDirection,
305        _fontWeight = fontWeight,
306        _fontStyle = fontStyle,
307        _maxLines = maxLines,
308        _fontFamily = fontFamily,
309        _fontSize = fontSize,
310        _height = height,
311        // TODO(b/128317744): add support for strut style.
312        _strutStyle = strutStyle,
313        _ellipsis = ellipsis,
314        _locale = locale;
315
316  final ui.TextAlign _textAlign;
317  final ui.TextDirection _textDirection;
318  final ui.FontWeight _fontWeight;
319  final ui.FontStyle _fontStyle;
320  final int _maxLines;
321  final String _fontFamily;
322  final double _fontSize;
323  final double _height;
324  final EngineStrutStyle _strutStyle;
325  final String _ellipsis;
326  final ui.Locale _locale;
327
328  String get _effectiveFontFamily {
329    if (assertionsEnabled) {
330      // In the flutter tester environment, we use a predictable-size font
331      // "Ahem". This makes widget tests predictable and less flaky.
332      if (ui.debugEmulateFlutterTesterEnvironment) {
333        return 'Ahem';
334      }
335    }
336    if (_fontFamily == null || _fontFamily.isEmpty) {
337      return DomRenderer.defaultFontFamily;
338    }
339    return _fontFamily;
340  }
341
342  double get _lineHeight {
343    if (_strutStyle == null || _strutStyle._height == null) {
344      // When there's no strut height, always use paragraph style height.
345      return _height;
346    }
347    if (_strutStyle._forceStrutHeight == true) {
348      // When strut height is forced, ignore paragraph style height.
349      return _strutStyle._height;
350    }
351    // In this case, strut height acts as a minimum height for all parts of the
352    // paragraph. So we take the max of strut height and paragraph style height.
353    return math.max(_strutStyle._height, _height ?? 0.0);
354  }
355
356  @override
357  bool operator ==(dynamic other) {
358    if (identical(this, other)) {
359      return true;
360    }
361    if (other.runtimeType != runtimeType) {
362      return false;
363    }
364    final EngineParagraphStyle typedOther = other;
365    return _textAlign == typedOther._textAlign ||
366        _textDirection == typedOther._textDirection ||
367        _fontWeight == typedOther._fontWeight ||
368        _fontStyle == typedOther._fontStyle ||
369        _maxLines == typedOther._maxLines ||
370        _fontFamily == typedOther._fontFamily ||
371        _fontSize == typedOther._fontSize ||
372        _height == typedOther._height ||
373        _ellipsis == typedOther._ellipsis ||
374        _locale == typedOther._locale;
375  }
376
377  @override
378  int get hashCode =>
379      ui.hashValues(_fontFamily, _fontSize, _height, _ellipsis, _locale);
380
381  @override
382  String toString() {
383    if (assertionsEnabled) {
384      return 'ParagraphStyle('
385          'textAlign: ${_textAlign ?? "unspecified"}, '
386          'textDirection: ${_textDirection ?? "unspecified"}, '
387          'fontWeight: ${_fontWeight ?? "unspecified"}, '
388          'fontStyle: ${_fontStyle ?? "unspecified"}, '
389          'maxLines: ${_maxLines ?? "unspecified"}, '
390          'fontFamily: ${_fontFamily ?? "unspecified"}, '
391          'fontSize: ${_fontSize != null ? _fontSize.toStringAsFixed(1) : "unspecified"}, '
392          'height: ${_height != null ? "${_height.toStringAsFixed(1)}x" : "unspecified"}, '
393          'ellipsis: ${_ellipsis != null ? "\"$_ellipsis\"" : "unspecified"}, '
394          'locale: ${_locale ?? "unspecified"}'
395          ')';
396    } else {
397      return super.toString();
398    }
399  }
400}
401
402/// The web implementation of [ui.TextStyle].
403class EngineTextStyle implements ui.TextStyle {
404  EngineTextStyle({
405    ui.Color color,
406    ui.TextDecoration decoration,
407    ui.Color decorationColor,
408    ui.TextDecorationStyle decorationStyle,
409    double decorationThickness,
410    ui.FontWeight fontWeight,
411    ui.FontStyle fontStyle,
412    ui.TextBaseline textBaseline,
413    String fontFamily,
414    List<String> fontFamilyFallback,
415    double fontSize,
416    double letterSpacing,
417    double wordSpacing,
418    double height,
419    ui.Locale locale,
420    ui.Paint background,
421    ui.Paint foreground,
422    List<ui.Shadow> shadows,
423    List<ui.FontFeature> fontFeatures,
424  })  : assert(
425            color == null || foreground == null,
426            'Cannot provide both a color and a foreground\n'
427            'The color argument is just a shorthand for "foreground: new Paint()..color = color".'),
428        _color = color,
429        _decoration = decoration,
430        _decorationColor = decorationColor,
431        _decorationStyle = decorationStyle,
432        _fontWeight = fontWeight,
433        _fontStyle = fontStyle,
434        _textBaseline = textBaseline,
435        // TODO(b/128311960): when font fallback is supported, we should check
436        //                    for it here.
437        _isFontFamilyProvided = fontFamily != null,
438        _fontFamily = fontFamily ?? '',
439        // TODO(b/128311960): add support for font family fallback.
440        _fontFamilyFallback = fontFamilyFallback,
441        _fontSize = fontSize,
442        _letterSpacing = letterSpacing,
443        _wordSpacing = wordSpacing,
444        _height = height,
445        _locale = locale,
446        _background = background,
447        _foreground = foreground,
448        _shadows = shadows;
449
450  final ui.Color _color;
451  final ui.TextDecoration _decoration;
452  final ui.Color _decorationColor;
453  final ui.TextDecorationStyle _decorationStyle;
454  final ui.FontWeight _fontWeight;
455  final ui.FontStyle _fontStyle;
456  final ui.TextBaseline _textBaseline;
457  final bool _isFontFamilyProvided;
458  final String _fontFamily;
459  final List<String> _fontFamilyFallback;
460  final double _fontSize;
461  final double _letterSpacing;
462  final double _wordSpacing;
463  final double _height;
464  final ui.Locale _locale;
465  final ui.Paint _background;
466  final ui.Paint _foreground;
467  final List<ui.Shadow> _shadows;
468
469  String get _effectiveFontFamily {
470    if (assertionsEnabled) {
471      // In the flutter tester environment, we use a predictable-size font
472      // "Ahem". This makes widget tests predictable and less flaky.
473      if (ui.debugEmulateFlutterTesterEnvironment) {
474        return 'Ahem';
475      }
476    }
477    if (_fontFamily == null || _fontFamily.isEmpty) {
478      return DomRenderer.defaultFontFamily;
479    }
480    return _fontFamily;
481  }
482
483  @override
484  bool operator ==(dynamic other) {
485    if (identical(this, other)) {
486      return true;
487    }
488    if (other.runtimeType != runtimeType) {
489      return false;
490    }
491    final EngineTextStyle typedOther = other;
492    return _color == typedOther._color &&
493        _decoration == typedOther._decoration &&
494        _decorationColor == typedOther._decorationColor &&
495        _decorationStyle == typedOther._decorationStyle &&
496        _fontWeight == typedOther._fontWeight &&
497        _fontStyle == typedOther._fontStyle &&
498        _textBaseline == typedOther._textBaseline &&
499        _fontFamily == typedOther._fontFamily &&
500        _fontSize == typedOther._fontSize &&
501        _letterSpacing == typedOther._letterSpacing &&
502        _wordSpacing == typedOther._wordSpacing &&
503        _height == typedOther._height &&
504        _locale == typedOther._locale &&
505        _background == typedOther._background &&
506        _foreground == typedOther._foreground &&
507        _listEquals<ui.Shadow>(_shadows, typedOther._shadows) &&
508        _listEquals<String>(
509            _fontFamilyFallback, typedOther._fontFamilyFallback);
510  }
511
512  @override
513  int get hashCode => ui.hashValues(
514        _color,
515        _decoration,
516        _decorationColor,
517        _decorationStyle,
518        _fontWeight,
519        _fontStyle,
520        _textBaseline,
521        _fontFamily,
522        _fontFamilyFallback,
523        _fontSize,
524        _letterSpacing,
525        _wordSpacing,
526        _height,
527        _locale,
528        _background,
529        _foreground,
530        _shadows,
531      );
532
533  @override
534  String toString() {
535    if (assertionsEnabled) {
536      return 'TextStyle('
537          'color: ${_color != null ? _color : "unspecified"}, '
538          'decoration: ${_decoration ?? "unspecified"}, '
539          'decorationColor: ${_decorationColor ?? "unspecified"}, '
540          'decorationStyle: ${_decorationStyle ?? "unspecified"}, '
541          'fontWeight: ${_fontWeight ?? "unspecified"}, '
542          'fontStyle: ${_fontStyle ?? "unspecified"}, '
543          'textBaseline: ${_textBaseline ?? "unspecified"}, '
544          'fontFamily: ${_isFontFamilyProvided && _fontFamily != null ? _fontFamily : "unspecified"}, '
545          'fontFamilyFallback: ${_isFontFamilyProvided && _fontFamilyFallback != null && _fontFamilyFallback.isNotEmpty ? _fontFamilyFallback : "unspecified"}, '
546          'fontSize: ${_fontSize != null ? _fontSize.toStringAsFixed(1) : "unspecified"}, '
547          'letterSpacing: ${_letterSpacing != null ? "${_letterSpacing}x" : "unspecified"}, '
548          'wordSpacing: ${_wordSpacing != null ? "${_wordSpacing}x" : "unspecified"}, '
549          'height: ${_height != null ? "${_height.toStringAsFixed(1)}x" : "unspecified"}, '
550          'locale: ${_locale ?? "unspecified"}, '
551          'background: ${_background ?? "unspecified"}, '
552          'foreground: ${_foreground ?? "unspecified"}, '
553          'shadows: ${_shadows ?? "unspecified"}'
554          ')';
555    } else {
556      return super.toString();
557    }
558  }
559}
560
561/// The web implementation of [ui.StrutStyle].
562class EngineStrutStyle implements ui.StrutStyle {
563  /// Creates a new StrutStyle object.
564  ///
565  /// * `fontFamily`: The name of the font to use when painting the text (e.g.,
566  ///   Roboto).
567  ///
568  /// * `fontFamilyFallback`: An ordered list of font family names that will be searched for when
569  ///    the font in `fontFamily` cannot be found.
570  ///
571  /// * `fontSize`: The size of glyphs (in logical pixels) to use when painting
572  ///   the text.
573  ///
574  /// * `lineHeight`: The minimum height of the line boxes, as a multiple of the
575  ///   font size. The lines of the paragraph will be at least
576  ///   `(lineHeight + leading) * fontSize` tall when fontSize
577  ///   is not null. When fontSize is null, there is no minimum line height. Tall
578  ///   glyphs due to baseline alignment or large [TextStyle.fontSize] may cause
579  ///   the actual line height after layout to be taller than specified here.
580  ///   [fontSize] must be provided for this property to take effect.
581  ///
582  /// * `leading`: The minimum amount of leading between lines as a multiple of
583  ///   the font size. [fontSize] must be provided for this property to take effect.
584  ///
585  /// * `fontWeight`: The typeface thickness to use when painting the text
586  ///   (e.g., bold).
587  ///
588  /// * `fontStyle`: The typeface variant to use when drawing the letters (e.g.,
589  ///   italics).
590  ///
591  /// * `forceStrutHeight`: When true, the paragraph will force all lines to be exactly
592  ///   `(lineHeight + leading) * fontSize` tall from baseline to baseline.
593  ///   [TextStyle] is no longer able to influence the line height, and any tall
594  ///   glyphs may overlap with lines above. If a [fontFamily] is specified, the
595  ///   total ascent of the first line will be the min of the `Ascent + half-leading`
596  ///   of the [fontFamily] and `(lineHeight + leading) * fontSize`. Otherwise, it
597  ///   will be determined by the Ascent + half-leading of the first text.
598  EngineStrutStyle({
599    String fontFamily,
600    List<String> fontFamilyFallback,
601    double fontSize,
602    double height,
603    double leading,
604    ui.FontWeight fontWeight,
605    ui.FontStyle fontStyle,
606    bool forceStrutHeight,
607  })  : _fontFamily = fontFamily,
608        _fontFamilyFallback = fontFamilyFallback,
609        _fontSize = fontSize,
610        _height = height,
611        _leading = leading,
612        _fontWeight = fontWeight,
613        _fontStyle = fontStyle,
614        _forceStrutHeight = forceStrutHeight;
615
616  final String _fontFamily;
617  final List<String> _fontFamilyFallback;
618  final double _fontSize;
619  final double _height;
620  final double _leading;
621  final ui.FontWeight _fontWeight;
622  final ui.FontStyle _fontStyle;
623  final bool _forceStrutHeight;
624
625  @override
626  bool operator ==(dynamic other) {
627    if (identical(this, other)) {
628      return true;
629    }
630    if (other.runtimeType != runtimeType) {
631      return false;
632    }
633    final EngineStrutStyle typedOther = other;
634    return _fontFamily == typedOther._fontFamily &&
635        _fontSize == typedOther._fontSize &&
636        _height == typedOther._height &&
637        _leading == typedOther._leading &&
638        _fontWeight == typedOther._fontWeight &&
639        _fontStyle == typedOther._fontStyle &&
640        _forceStrutHeight == typedOther._forceStrutHeight &&
641        _listEquals<String>(
642            _fontFamilyFallback, typedOther._fontFamilyFallback);
643  }
644
645  @override
646  int get hashCode => ui.hashValues(
647        _fontFamily,
648        _fontFamilyFallback,
649        _fontSize,
650        _height,
651        _leading,
652        _fontWeight,
653        _fontStyle,
654        _forceStrutHeight,
655      );
656}
657
658/// The web implementation of [ui.ParagraphBuilder].
659class EngineParagraphBuilder implements ui.ParagraphBuilder {
660  /// Marks a call to the [pop] method in the [_ops] list.
661  static final Object _paragraphBuilderPop = Object();
662
663  final html.HtmlElement _paragraphElement = domRenderer.createElement('p');
664  final EngineParagraphStyle _paragraphStyle;
665  final List<dynamic> _ops = <dynamic>[];
666
667  /// Creates an [EngineParagraphBuilder] object, which is used to create a
668  /// [EngineParagraph].
669  EngineParagraphBuilder(EngineParagraphStyle style) : _paragraphStyle = style {
670    // TODO(b/128317744): Implement support for strut font families.
671    List<String> strutFontFamilies;
672    if (style._strutStyle != null) {
673      strutFontFamilies = <String>[];
674      if (style._strutStyle._fontFamily != null) {
675        strutFontFamilies.add(style._strutStyle._fontFamily);
676      }
677      if (style._strutStyle._fontFamilyFallback != null) {
678        strutFontFamilies.addAll(style._strutStyle._fontFamilyFallback);
679      }
680    }
681    _applyParagraphStyleToElement(
682        element: _paragraphElement, style: _paragraphStyle);
683  }
684
685  /// Applies the given style to the added text until [pop] is called.
686  ///
687  /// See [pop] for details.
688  @override
689  void pushStyle(ui.TextStyle style) {
690    _ops.add(style);
691  }
692
693  @override
694  int get placeholderCount => _placeholderCount;
695  int _placeholderCount;
696
697  @override
698  List<double> get placeholderScales => _placeholderScales;
699  List<double> _placeholderScales = <double>[];
700
701  @override
702  void addPlaceholder(
703    double width,
704    double height,
705    ui.PlaceholderAlignment alignment, {
706    double scale,
707    double baselineOffset,
708    ui.TextBaseline baseline,
709  }) {
710    // TODO(garyq): Implement web_ui version of this.
711    throw UnimplementedError();
712  }
713
714  // TODO(yjbanov): do we need to do this?
715//  static String _encodeLocale(Locale locale) => locale?.toString() ?? '';
716
717  /// Ends the effect of the most recent call to [pushStyle].
718  ///
719  /// Internally, the paragraph builder maintains a stack of text styles. Text
720  /// added to the paragraph is affected by all the styles in the stack. Calling
721  /// [pop] removes the topmost style in the stack, leaving the remaining styles
722  /// in effect.
723  @override
724  void pop() {
725    _ops.add(_paragraphBuilderPop);
726  }
727
728  /// Adds the given text to the paragraph.
729  ///
730  /// The text will be styled according to the current stack of text styles.
731  @override
732  void addText(String text) {
733    _ops.add(text);
734  }
735
736  /// Applies the given paragraph style and returns a [Paragraph] containing the
737  /// added text and associated styling.
738  ///
739  /// After calling this function, the paragraph builder object is invalid and
740  /// cannot be used further.
741  @override
742  EngineParagraph build() {
743    return _tryBuildPlainText() ?? _buildRichText();
744  }
745
746  /// Attempts to build a [Paragraph] assuming it is plain text.
747  ///
748  /// A paragraph is considered plain if it is built using the following
749  /// sequence of ops:
750  ///
751  /// * Zero-or-more calls to [pushStyle].
752  /// * One-or-more calls to [addText].
753  /// * Zero-or-more calls to [pop].
754  ///
755  /// Any other sequence will result in `null` and should be treated as rich
756  /// text.
757  ///
758  /// Plain text is not the same as not having style. The text may be styled
759  /// arbitrarily. However, it may not mix multiple styles in the same
760  /// paragraph. Plain text is more efficient to lay out and measure than rich
761  /// text.
762  EngineParagraph _tryBuildPlainText() {
763    ui.Color color;
764    ui.TextDecoration decoration;
765    ui.Color decorationColor;
766    ui.TextDecorationStyle decorationStyle;
767    ui.FontWeight fontWeight = _paragraphStyle._fontWeight;
768    ui.FontStyle fontStyle = _paragraphStyle._fontStyle;
769    ui.TextBaseline textBaseline;
770    String fontFamily = _paragraphStyle._fontFamily;
771    double fontSize = _paragraphStyle._fontSize;
772    final ui.TextAlign textAlign = _paragraphStyle._textAlign;
773    final ui.TextDirection textDirection = _paragraphStyle._textDirection;
774    double letterSpacing;
775    double wordSpacing;
776    double height;
777    ui.Locale locale = _paragraphStyle._locale;
778    ui.Paint background;
779    ui.Paint foreground;
780
781    int i = 0;
782
783    // This loop looks expensive. However, in reality most of plain text
784    // paragraphs will have no calls to [pushStyle], skipping this loop
785    // entirely. Occasionally there will be one [pushStyle], which causes this
786    // loop to run once then move on to aggregating text.
787    while (i < _ops.length && _ops[i] is EngineTextStyle) {
788      final EngineTextStyle style = _ops[i];
789      if (style._color != null) {
790        color = style._color;
791      }
792      if (style._decoration != null) {
793        decoration = style._decoration;
794      }
795      if (style._decorationColor != null) {
796        decorationColor = style._decorationColor;
797      }
798      if (style._decorationStyle != null) {
799        decorationStyle = style._decorationStyle;
800      }
801      if (style._fontWeight != null) {
802        fontWeight = style._fontWeight;
803      }
804      if (style._fontStyle != null) {
805        fontStyle = style._fontStyle;
806      }
807      if (style._textBaseline != null) {
808        textBaseline = style._textBaseline;
809      }
810      if (style._fontFamily != null) {
811        fontFamily = style._fontFamily;
812      }
813      if (style._fontSize != null) {
814        fontSize = style._fontSize;
815      }
816      if (style._letterSpacing != null) {
817        letterSpacing = style._letterSpacing;
818      }
819      if (style._wordSpacing != null) {
820        wordSpacing = style._wordSpacing;
821      }
822      if (style._height != null) {
823        height = style._height;
824      }
825      if (style._locale != null) {
826        locale = style._locale;
827      }
828      if (style._background != null) {
829        background = style._background;
830      }
831      if (style._foreground != null) {
832        foreground = style._foreground;
833      }
834      i++;
835    }
836
837    final EngineTextStyle cumulativeStyle = EngineTextStyle(
838      color: color,
839      decoration: decoration,
840      decorationColor: decorationColor,
841      decorationStyle: decorationStyle,
842      fontWeight: fontWeight,
843      fontStyle: fontStyle,
844      textBaseline: textBaseline,
845      fontFamily: fontFamily,
846      fontSize: fontSize,
847      letterSpacing: letterSpacing,
848      wordSpacing: wordSpacing,
849      height: height,
850      locale: locale,
851      background: background,
852      foreground: foreground,
853    );
854
855    ui.Paint paint;
856    if (foreground != null) {
857      paint = foreground;
858    } else {
859      paint = ui.Paint();
860      if (color != null) {
861        paint.color = color;
862      }
863    }
864
865    if (i >= _ops.length) {
866      // Empty paragraph.
867      _applyTextStyleToElement(
868          element: _paragraphElement, style: cumulativeStyle);
869      return EngineParagraph(
870        paragraphElement: _paragraphElement,
871        geometricStyle: ParagraphGeometricStyle(
872          fontFamily: fontFamily,
873          fontWeight: fontWeight,
874          fontStyle: fontStyle,
875          fontSize: fontSize,
876          lineHeight: height,
877          maxLines: _paragraphStyle._maxLines,
878          letterSpacing: letterSpacing,
879          wordSpacing: wordSpacing,
880          decoration: _textDecorationToCssString(decoration, decorationStyle),
881          ellipsis: _paragraphStyle._ellipsis,
882        ),
883        plainText: '',
884        paint: paint,
885        textAlign: textAlign,
886        textDirection: textDirection,
887        background: cumulativeStyle._background,
888      );
889    }
890
891    if (_ops[i] is! String) {
892      // After a series of [EngineTextStyle] ops there must be at least one text op.
893      // Otherwise, treat it as rich text.
894      return null;
895    }
896
897    // Accumulate text into one contiguous string.
898    final StringBuffer plainTextBuffer = StringBuffer();
899    while (i < _ops.length && _ops[i] is String) {
900      plainTextBuffer.write(_ops[i]);
901      i++;
902    }
903
904    // After a series of [addText] ops there should only be a tail of [pop]s and
905    // nothing else. Otherwise it's rich text and we return null;
906    for (; i < _ops.length; i++) {
907      if (_ops[i] != _paragraphBuilderPop) {
908        return null;
909      }
910    }
911
912    final String plainText = plainTextBuffer.toString();
913    domRenderer.appendText(_paragraphElement, plainText);
914    _applyTextStyleToElement(
915        element: _paragraphElement, style: cumulativeStyle);
916    // Since this is a plain paragraph apply background color to paragraph tag
917    // instead of individual spans.
918    if (cumulativeStyle._background != null) {
919      _applyTextBackgroundToElement(
920          element: _paragraphElement, style: cumulativeStyle);
921    }
922    return EngineParagraph(
923      paragraphElement: _paragraphElement,
924      geometricStyle: ParagraphGeometricStyle(
925        fontFamily: fontFamily,
926        fontWeight: fontWeight,
927        fontStyle: fontStyle,
928        fontSize: fontSize,
929        lineHeight: height,
930        maxLines: _paragraphStyle._maxLines,
931        letterSpacing: letterSpacing,
932        wordSpacing: wordSpacing,
933        decoration: _textDecorationToCssString(decoration, decorationStyle),
934        ellipsis: _paragraphStyle._ellipsis,
935      ),
936      plainText: plainText,
937      paint: paint,
938      textAlign: textAlign,
939      textDirection: textDirection,
940      background: cumulativeStyle._background,
941    );
942  }
943
944  /// Builds a [Paragraph] as rich text.
945  EngineParagraph _buildRichText() {
946    final List<dynamic> elementStack = <dynamic>[];
947    dynamic currentElement() =>
948        elementStack.isNotEmpty ? elementStack.last : _paragraphElement;
949    for (int i = 0; i < _ops.length; i++) {
950      final dynamic op = _ops[i];
951      if (op is EngineTextStyle) {
952        final html.SpanElement span = domRenderer.createElement('span');
953        _applyTextStyleToElement(element: span, style: op);
954        if (op._background != null) {
955          _applyTextBackgroundToElement(element: span, style: op);
956        }
957        domRenderer.append(currentElement(), span);
958        elementStack.add(span);
959      } else if (op is String) {
960        domRenderer.appendText(currentElement(), op);
961      } else if (identical(op, _paragraphBuilderPop)) {
962        elementStack.removeLast();
963      } else {
964        throw UnsupportedError('Unsupported ParagraphBuilder operation: $op');
965      }
966    }
967
968    return EngineParagraph(
969      paragraphElement: _paragraphElement,
970      geometricStyle: ParagraphGeometricStyle(
971        fontFamily: _paragraphStyle._fontFamily,
972        fontWeight: _paragraphStyle._fontWeight,
973        fontStyle: _paragraphStyle._fontStyle,
974        fontSize: _paragraphStyle._fontSize,
975        lineHeight: _paragraphStyle._height,
976        maxLines: _paragraphStyle._maxLines,
977        ellipsis: _paragraphStyle._ellipsis,
978      ),
979      plainText: null,
980      paint: null,
981      textAlign: _paragraphStyle._textAlign,
982      textDirection: _paragraphStyle._textDirection,
983      background: null,
984    );
985  }
986}
987
988/// Converts [fontWeight] to its CSS equivalent value.
989String fontWeightToCss(ui.FontWeight fontWeight) {
990  if (fontWeight == null) {
991    return null;
992  }
993
994  switch (fontWeight.index) {
995    case 0:
996      return '100';
997    case 1:
998      return '200';
999    case 2:
1000      return '300';
1001    case 3:
1002      return 'normal';
1003    case 4:
1004      return '500';
1005    case 5:
1006      return '600';
1007    case 6:
1008      return 'bold';
1009    case 7:
1010      return '800';
1011    case 8:
1012      return '900';
1013  }
1014
1015  assert(() {
1016    throw AssertionError(
1017      'Failed to convert font weight $fontWeight to CSS.',
1018    );
1019  }());
1020
1021  return '';
1022}
1023
1024/// Applies a paragraph [style] to an [element], translating the properties to
1025/// their corresponding CSS equivalents.
1026///
1027/// If [previousStyle] is not null, updates only the mismatching attributes.
1028void _applyParagraphStyleToElement({
1029  @required html.HtmlElement element,
1030  @required EngineParagraphStyle style,
1031  EngineParagraphStyle previousStyle,
1032}) {
1033  assert(element != null);
1034  assert(style != null);
1035  // TODO(yjbanov): What do we do about ParagraphStyle._locale and ellipsis?
1036  final html.CssStyleDeclaration cssStyle = element.style;
1037  if (previousStyle == null) {
1038    if (style._textAlign != null) {
1039      cssStyle.textAlign = _textAlignToCssValue(
1040          style._textAlign, style._textDirection ?? ui.TextDirection.ltr);
1041    }
1042    if (style._lineHeight != null) {
1043      cssStyle.lineHeight = '${style._lineHeight}';
1044    }
1045    if (style._textDirection != null) {
1046      cssStyle.direction = _textDirectionToCssValue(style._textDirection);
1047    }
1048    if (style._fontSize != null) {
1049      cssStyle.fontSize = '${style._fontSize.floor()}px';
1050    }
1051    if (style._fontWeight != null) {
1052      cssStyle.fontWeight = fontWeightToCss(style._fontWeight);
1053    }
1054    if (style._fontStyle != null) {
1055      cssStyle.fontStyle =
1056          style._fontStyle == ui.FontStyle.normal ? 'normal' : 'italic';
1057    }
1058    if (style._effectiveFontFamily != null) {
1059      cssStyle.fontFamily = style._effectiveFontFamily;
1060    }
1061  } else {
1062    if (style._textAlign != previousStyle._textAlign) {
1063      cssStyle.textAlign = _textAlignToCssValue(
1064          style._textAlign, style._textDirection ?? ui.TextDirection.ltr);
1065    }
1066    if (style._lineHeight != style._lineHeight) {
1067      cssStyle.lineHeight = '${style._lineHeight}';
1068    }
1069    if (style._textDirection != previousStyle._textDirection) {
1070      cssStyle.direction = _textDirectionToCssValue(style._textDirection);
1071    }
1072    if (style._fontSize != previousStyle._fontSize) {
1073      cssStyle.fontSize =
1074          style._fontSize != null ? '${style._fontSize.floor()}px' : null;
1075    }
1076    if (style._fontWeight != previousStyle._fontWeight) {
1077      cssStyle.fontWeight = fontWeightToCss(style._fontWeight);
1078    }
1079    if (style._fontStyle != previousStyle._fontStyle) {
1080      cssStyle.fontStyle = style._fontStyle != null
1081          ? (style._fontStyle == ui.FontStyle.normal ? 'normal' : 'italic')
1082          : null;
1083    }
1084    if (style._fontFamily != previousStyle._fontFamily) {
1085      cssStyle.fontFamily = style._fontFamily;
1086    }
1087  }
1088}
1089
1090/// Applies a text [style] to an [element], translating the properties to their
1091/// corresponding CSS equivalents.
1092///
1093/// If [previousStyle] is not null, updates only the mismatching attributes.
1094void _applyTextStyleToElement({
1095  @required html.HtmlElement element,
1096  @required EngineTextStyle style,
1097  EngineTextStyle previousStyle,
1098}) {
1099  assert(element != null);
1100  assert(style != null);
1101  bool updateDecoration = false;
1102  final html.CssStyleDeclaration cssStyle = element.style;
1103  if (previousStyle == null) {
1104    final ui.Color color = style._foreground?.color ?? style._color;
1105    if (color != null) {
1106      cssStyle.color = color.toCssString();
1107    }
1108    if (style._fontSize != null) {
1109      cssStyle.fontSize = '${style._fontSize.floor()}px';
1110    }
1111    if (style._fontWeight != null) {
1112      cssStyle.fontWeight = fontWeightToCss(style._fontWeight);
1113    }
1114    if (style._fontStyle != null) {
1115      cssStyle.fontStyle =
1116          style._fontStyle == ui.FontStyle.normal ? 'normal' : 'italic';
1117    }
1118    if (style._effectiveFontFamily != null) {
1119      cssStyle.fontFamily = style._effectiveFontFamily;
1120    }
1121    if (style._letterSpacing != null) {
1122      cssStyle.letterSpacing = '${style._letterSpacing}px';
1123    }
1124    if (style._wordSpacing != null) {
1125      cssStyle.wordSpacing = '${style._wordSpacing}px';
1126    }
1127    if (style._decoration != null) {
1128      updateDecoration = true;
1129    }
1130  } else {
1131    if (style._color != previousStyle._color ||
1132        style._foreground != previousStyle._foreground) {
1133      final ui.Color color = style._foreground?.color ?? style._color;
1134      cssStyle.color = color?.toCssString();
1135    }
1136
1137    if (style._fontSize != previousStyle._fontSize) {
1138      cssStyle.fontSize =
1139          style._fontSize != null ? '${style._fontSize.floor()}px' : null;
1140    }
1141
1142    if (style._fontWeight != previousStyle._fontWeight) {
1143      cssStyle.fontWeight = fontWeightToCss(style._fontWeight);
1144    }
1145
1146    if (style._fontStyle != previousStyle._fontStyle) {
1147      cssStyle.fontStyle = style._fontStyle != null
1148          ? style._fontStyle == ui.FontStyle.normal ? 'normal' : 'italic'
1149          : null;
1150    }
1151    if (style._fontFamily != previousStyle._fontFamily) {
1152      cssStyle.fontFamily = style._fontFamily;
1153    }
1154    if (style._letterSpacing != previousStyle._letterSpacing) {
1155      cssStyle.letterSpacing = '${style._letterSpacing}px';
1156    }
1157    if (style._wordSpacing != previousStyle._wordSpacing) {
1158      cssStyle.wordSpacing = '${style._wordSpacing}px';
1159    }
1160    if (style._decoration != previousStyle._decoration ||
1161        style._decorationStyle != previousStyle._decorationStyle ||
1162        style._decorationColor != previousStyle._decorationColor) {
1163      updateDecoration = true;
1164    }
1165  }
1166
1167  if (updateDecoration) {
1168    if (style._decoration != null) {
1169      final String textDecoration =
1170          _textDecorationToCssString(style._decoration, style._decorationStyle);
1171      if (textDecoration != null) {
1172        cssStyle.textDecoration = textDecoration;
1173        final ui.Color decorationColor = style._decorationColor;
1174        if (decorationColor != null) {
1175          cssStyle.textDecorationColor = decorationColor.toCssString();
1176        }
1177      }
1178    }
1179  }
1180}
1181
1182/// Applies background color properties in text style to paragraph or span
1183/// elements.
1184void _applyTextBackgroundToElement({
1185  @required html.HtmlElement element,
1186  @required EngineTextStyle style,
1187  EngineTextStyle previousStyle,
1188}) {
1189  final ui.Paint newBackground = style._background;
1190  if (previousStyle == null) {
1191    if (newBackground != null) {
1192      domRenderer.setElementStyle(
1193          element, 'background-color', newBackground.color.toCssString());
1194    }
1195  } else {
1196    if (newBackground != previousStyle._background) {
1197      domRenderer.setElementStyle(
1198          element, 'background-color', newBackground.color?.toCssString());
1199    }
1200  }
1201}
1202
1203/// Converts text decoration style to CSS text-decoration-style value.
1204String _textDecorationToCssString(
1205    ui.TextDecoration decoration, ui.TextDecorationStyle decorationStyle) {
1206  final StringBuffer decorations = StringBuffer();
1207  if (decoration != null) {
1208    if (decoration.contains(ui.TextDecoration.underline)) {
1209      decorations.write('underline ');
1210    }
1211    if (decoration.contains(ui.TextDecoration.overline)) {
1212      decorations.write('overline ');
1213    }
1214    if (decoration.contains(ui.TextDecoration.lineThrough)) {
1215      decorations.write('line-through ');
1216    }
1217  }
1218  if (decorationStyle != null) {
1219    decorations.write(_decorationStyleToCssString(decorationStyle));
1220  }
1221  return decorations.isEmpty ? null : decorations.toString();
1222}
1223
1224String _decorationStyleToCssString(ui.TextDecorationStyle decorationStyle) {
1225  switch (decorationStyle) {
1226    case ui.TextDecorationStyle.dashed:
1227      return 'dashed';
1228    case ui.TextDecorationStyle.dotted:
1229      return 'dotted';
1230    case ui.TextDecorationStyle.double:
1231      return 'double';
1232    case ui.TextDecorationStyle.solid:
1233      return 'solid';
1234    case ui.TextDecorationStyle.wavy:
1235      return 'wavy';
1236    default:
1237      return null;
1238  }
1239}
1240
1241/// Converts [textDirection] to its corresponding CSS value.
1242///
1243/// This value is used for the "direction" CSS property, e.g.:
1244///
1245/// ```css
1246/// direction: rtl;
1247/// ```
1248String _textDirectionToCssValue(ui.TextDirection textDirection) {
1249  return textDirection == ui.TextDirection.ltr
1250      ? null // it's the default
1251      : 'rtl';
1252}
1253
1254/// Converts [align] to its corresponding CSS value.
1255///
1256/// This value is used as the "text-align" CSS property, e.g.:
1257///
1258/// ```css
1259/// text-align: right;
1260/// ```
1261String _textAlignToCssValue(
1262    ui.TextAlign align, ui.TextDirection textDirection) {
1263  switch (align) {
1264    case ui.TextAlign.left:
1265      return 'left';
1266    case ui.TextAlign.right:
1267      return 'right';
1268    case ui.TextAlign.center:
1269      return 'center';
1270    case ui.TextAlign.justify:
1271      return 'justify';
1272    case ui.TextAlign.start:
1273      switch (textDirection) {
1274        case ui.TextDirection.ltr:
1275          return null; // it's the default
1276        case ui.TextDirection.rtl:
1277          return 'right';
1278      }
1279      break;
1280    case ui.TextAlign.end:
1281      switch (textDirection) {
1282        case ui.TextDirection.ltr:
1283          return 'end';
1284        case ui.TextDirection.rtl:
1285          return 'left';
1286      }
1287      break;
1288  }
1289  throw AssertionError('Unsupported TextAlign value $align');
1290}
1291
1292/// Determines if lists [a] and [b] are deep equivalent.
1293///
1294/// Returns true if the lists are both null, or if they are both non-null, have
1295/// the same length, and contain the same elements in the same order. Returns
1296/// false otherwise.
1297bool _listEquals<T>(List<T> a, List<T> b) {
1298  if (a == null) {
1299    return b == null;
1300  }
1301  if (b == null || a.length != b.length) {
1302    return false;
1303  }
1304  for (int index = 0; index < a.length; index += 1) {
1305    if (a[index] != b[index]) {
1306      return false;
1307    }
1308  }
1309  return true;
1310}
1311