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