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/// Contains the subset of [ui.ParagraphStyle] properties that affect layout. 8class ParagraphGeometricStyle { 9 ParagraphGeometricStyle({ 10 this.fontWeight, 11 this.fontStyle, 12 this.fontFamily, 13 this.fontSize, 14 this.lineHeight, 15 this.maxLines, 16 this.letterSpacing, 17 this.wordSpacing, 18 this.decoration, 19 this.ellipsis, 20 }); 21 22 final ui.FontWeight fontWeight; 23 final ui.FontStyle fontStyle; 24 final String fontFamily; 25 final double fontSize; 26 final double lineHeight; 27 final int maxLines; 28 final double letterSpacing; 29 final double wordSpacing; 30 final String decoration; 31 final String ellipsis; 32 33 // Since all fields above are primitives, cache hashcode since ruler lookups 34 // use this style as key. 35 int _cachedHashCode; 36 37 /// Returns the font-family that should be used to style the paragraph. It may 38 /// or may not be different from [fontFamily]: 39 /// 40 /// - Always returns "Ahem" in tests. 41 /// - Provides correct defaults when [fontFamily] doesn't have a value. 42 String get effectiveFontFamily { 43 if (assertionsEnabled) { 44 // In widget tests we use a predictable-size font "Ahem". This makes 45 // widget tests predictable and less flaky. 46 if (ui.debugEmulateFlutterTesterEnvironment) { 47 return 'Ahem'; 48 } 49 } 50 if (fontFamily == null || fontFamily.isEmpty) { 51 return DomRenderer.defaultFontFamily; 52 } 53 return fontFamily; 54 } 55 56 String _cssFontString; 57 58 /// Cached font string that can be used in CSS. 59 /// 60 /// See <https://developer.mozilla.org/en-US/docs/Web/CSS/font>. 61 String get cssFontString => _cssFontString ??= _buildCssFontString(); 62 63 String _buildCssFontString() { 64 final StringBuffer result = StringBuffer(); 65 66 // Font style 67 if (fontStyle != null) { 68 result.write(fontStyle == ui.FontStyle.normal ? 'normal' : 'italic'); 69 } else { 70 result.write(DomRenderer.defaultFontStyle); 71 } 72 result.write(' '); 73 74 // Font weight. 75 if (fontWeight != null) { 76 result.write(fontWeightToCss(fontWeight)); 77 } else { 78 result.write(DomRenderer.defaultFontWeight); 79 } 80 result.write(' '); 81 82 if (fontSize != null) { 83 result.write(fontSize.floor()); 84 result.write('px'); 85 } else { 86 result.write(DomRenderer.defaultFontSize); 87 } 88 result.write(' '); 89 result.write(effectiveFontFamily); 90 91 return result.toString(); 92 } 93 94 @override 95 bool operator ==(dynamic other) { 96 if (identical(this, other)) { 97 return true; 98 } 99 if (other.runtimeType != runtimeType) { 100 return false; 101 } 102 final ParagraphGeometricStyle typedOther = other; 103 return fontWeight == typedOther.fontWeight && 104 fontStyle == typedOther.fontStyle && 105 fontFamily == typedOther.fontFamily && 106 fontSize == typedOther.fontSize && 107 lineHeight == typedOther.lineHeight && 108 maxLines == typedOther.maxLines && 109 letterSpacing == typedOther.letterSpacing && 110 wordSpacing == typedOther.wordSpacing && 111 decoration == typedOther.decoration && 112 ellipsis == typedOther.ellipsis; 113 } 114 115 @override 116 int get hashCode => _cachedHashCode ??= ui.hashValues( 117 fontWeight, 118 fontStyle, 119 fontFamily, 120 fontSize, 121 lineHeight, 122 maxLines, 123 letterSpacing, 124 wordSpacing, 125 decoration, 126 ellipsis, 127 ); 128 129 @override 130 String toString() { 131 if (assertionsEnabled) { 132 return '$runtimeType(fontWeight: $fontWeight, fontStyle: $fontStyle,' 133 ' fontFamily: $fontFamily, fontSize: $fontSize,' 134 ' lineHeight: $lineHeight,' 135 ' maxLines: $maxLines,' 136 ' letterSpacing: $letterSpacing,' 137 ' wordSpacing: $wordSpacing,' 138 ' decoration: $decoration,' 139 ' ellipsis: $ellipsis,' 140 ')'; 141 } else { 142 return super.toString(); 143 } 144 } 145} 146 147/// Provides text dimensions found on [_element]. The idea behind this class is 148/// to allow the [ParagraphRuler] to mutate multiple dom elements and allow 149/// consumers to lazily read the measurements. 150/// 151/// The [ParagraphRuler] would have multiple instances of [TextDimensions] with 152/// different backing elements for different types of measurements. When a 153/// measurement is needed, the [ParagraphRuler] would mutate all the backing 154/// elements at once. The consumer of the ruler can later read those 155/// measurements. 156/// 157/// The rationale behind this is to minimize browser reflows by batching dom 158/// writes first, then performing all the reads. 159class TextDimensions { 160 TextDimensions(this._element); 161 162 final html.HtmlElement _element; 163 html.Rectangle<num> _cachedBoundingClientRect; 164 165 /// Attempts to efficiently copy text from [from]. 166 /// 167 /// The primary efficiency gain is from rare occurrence of rich text in 168 /// typical apps. 169 void updateText(EngineParagraph from, ParagraphGeometricStyle style) { 170 assert(from != null); 171 assert(_element != null); 172 assert(from._debugHasSameRootStyle(style)); 173 assert(() { 174 final bool wasEmptyOrPlainText = _element.childNodes.isEmpty || 175 (_element.childNodes.length == 1 && 176 _element.childNodes.first is html.Text); 177 if (!wasEmptyOrPlainText) { 178 throw Exception( 179 'Failed to copy text into the paragraph measuring element. The ' 180 'element already contains rich text "${_element.innerHtml}". It is ' 181 'likely that a previous measurement did not clean up after ' 182 'itself.'); 183 } 184 return true; 185 }()); 186 187 _invalidateBoundsCache(); 188 final String plainText = from._plainText; 189 if (plainText != null) { 190 // Plain text: just set the string. The paragraph's style is assumed to 191 // match the style set on the `element`. Setting text as plain string is 192 // faster because it doesn't change the DOM structure or CSS attributes, 193 // and therefore doesn't trigger style recalculations in the browser. 194 _element.text = plainText; 195 } else { 196 // Rich text: deeply copy contents. This is the slow case that should be 197 // avoided if fast layout performance is desired. 198 final html.Element copy = from._paragraphElement.clone(true); 199 _element.children.addAll(copy.children); 200 } 201 } 202 203 /// Updated element style width. 204 void updateWidth(String cssWidth) { 205 _invalidateBoundsCache(); 206 _element.style.width = cssWidth; 207 } 208 209 void _invalidateBoundsCache() { 210 _cachedBoundingClientRect = null; 211 } 212 213 /// Sets text of contents to a single space character to measure empty text. 214 void updateTextToSpace() { 215 _invalidateBoundsCache(); 216 _element.text = ' '; 217 } 218 219 /// Applies geometric style properties to the [element]. 220 void applyStyle(ParagraphGeometricStyle style) { 221 _element.style 222 ..fontSize = style.fontSize != null ? '${style.fontSize.floor()}px' : null 223 ..fontFamily = style.effectiveFontFamily 224 ..fontWeight = 225 style.fontWeight != null ? fontWeightToCss(style.fontWeight) : null 226 ..fontStyle = style.fontStyle != null 227 ? style.fontStyle == ui.FontStyle.normal ? 'normal' : 'italic' 228 : null 229 ..letterSpacing = 230 style.letterSpacing != null ? '${style.letterSpacing}px' : null 231 ..wordSpacing = 232 style.wordSpacing != null ? '${style.wordSpacing}px' : null 233 ..textDecoration = style.decoration; 234 if (style.lineHeight != null) { 235 _element.style.lineHeight = style.lineHeight.toString(); 236 } 237 _invalidateBoundsCache(); 238 } 239 240 /// Appends element and probe to hostElement that is setup for a specific 241 /// TextStyle. 242 void appendToHost(html.HtmlElement hostElement) { 243 hostElement.append(_element); 244 _invalidateBoundsCache(); 245 } 246 247 html.Rectangle<num> _readAndCacheMetrics() => 248 _cachedBoundingClientRect ??= _element.getBoundingClientRect(); 249 250 /// The width of the paragraph being measured. 251 double get width => _readAndCacheMetrics().width; 252 253 /// The height of the paragraph being measured. 254 double get height => _readAndCacheMetrics().height; 255} 256 257/// Performs 4 types of measurements: 258/// 259/// 1. Single line: can be prepared by calling [measureAsSingleLine]. 260/// Measurement values will be available at [singleLineDimensions]. 261/// 262/// 2. Minimum intrinsic width: can be prepared by calling 263/// [measureMinIntrinsicWidth]. Measurement values will be available at 264/// [minIntrinsicDimensions]. 265/// 266/// 3. Constrained: can be prepared by calling [measureWithConstraints] and 267/// passing the constraints. Measurement values will be available at 268/// [constrainedDimensions]. 269/// 270/// 4. Boxes: within a paragraph, it measures a list of text boxes that enclose 271/// a given range of text. 272/// 273/// For performance reasons, it's advised to use [measureAll] and then reading 274/// whatever measurements are needed. This causes the browser to only reflow 275/// once instead of many times. 276/// 277/// The [measureAll] method performs the first 3 stateful measurements but not 278/// the 4th one. 279/// 280/// This class is both reusable and stateful. Use it carefully. The correct 281/// usage is as follows: 282/// 283/// * First, call [willMeasure] passing it the paragraph to be measured. 284/// * Call any of the [measureAsSingleLine], [measureMinIntrinsicWidth], 285/// [measureWithConstraints], or [measureAll], to prepare the respective 286/// measurement. These methods can be called any number of times. 287/// * Call [didMeasure] to indicate that you are done with the paragraph passed 288/// to the [willMeasure] method. 289/// 290/// It is safe to reuse this object as long as paragraphs passed to the 291/// [measure] method have the same style. 292/// 293/// The only stateless method provided by this class is [measureBoxesForRange] 294/// that doesn't rely on [willMeasure] and [didMeasure] lifecycle methods. 295/// 296/// This class optimizes for plain text paragraphs, which should constitute the 297/// majority of paragraphs in typical apps. 298class ParagraphRuler { 299 /// The only style that this [ParagraphRuler] measures text. 300 final ParagraphGeometricStyle style; 301 302 /// A [RulerManager] owns the host DOM element that this ruler can add 303 /// elements to. 304 /// 305 /// The [rulerManager] keeps a cache of multiple [ParagraphRuler] instances, 306 /// but a [ParagraphRuler] can only belong to one [RulerManager]. 307 final RulerManager rulerManager; 308 309 /// Probe to use for measuring alphabetic base line. 310 final html.HtmlElement _probe = html.DivElement(); 311 312 /// Cached value of alphabetic base line. 313 double _cachedAlphabeticBaseline; 314 315 ParagraphRuler(this.style, this.rulerManager) { 316 _configureSingleLineHostElements(); 317 // Since alphabeticbaseline will be same regardless of constraints. 318 // We can measure it using a probe on the single line dimensions 319 // host. 320 _singleLineHost.append(_probe); 321 _configureMinIntrinsicHostElements(); 322 _configureConstrainedHostElements(); 323 } 324 325 /// The alphabetic baseline of the paragraph being measured. 326 double get alphabeticBaseline => 327 _cachedAlphabeticBaseline ??= _probe.getBoundingClientRect().bottom; 328 329 // Elements used to measure single-line metrics. 330 final html.DivElement _singleLineHost = html.DivElement(); 331 final TextDimensions singleLineDimensions = 332 TextDimensions(html.ParagraphElement()); 333 334 // Elements used to measure minIntrinsicWidth. 335 final html.DivElement _minIntrinsicHost = html.DivElement(); 336 TextDimensions minIntrinsicDimensions = 337 TextDimensions(html.ParagraphElement()); 338 339 // Elements used to measure metrics under a width constraint. 340 final html.DivElement _constrainedHost = html.DivElement(); 341 TextDimensions constrainedDimensions = 342 TextDimensions(html.ParagraphElement()); 343 344 // Elements used to measure the line-height metric. 345 html.DivElement _lineHeightHost; 346 TextDimensions _lineHeightDimensions; 347 TextDimensions get lineHeightDimensions { 348 // Lazily create the elements for line-height measurement since they are not 349 // always needed. 350 if (_lineHeightDimensions == null) { 351 _lineHeightHost = html.DivElement(); 352 _lineHeightDimensions = TextDimensions(html.ParagraphElement()); 353 _configureLineHeightHostElements(); 354 _lineHeightHost.append(_probe); 355 } 356 return _lineHeightDimensions; 357 } 358 359 /// The number of times this ruler was used this frame. 360 /// 361 /// This value is used to determine which rulers are rarely used and should be 362 /// evicted from the ruler cache. 363 int get hitCount => _hitCount; 364 int _hitCount = 0; 365 366 /// This method should be called whenever this ruler is being used to perform 367 /// measurements. 368 /// 369 /// It increases the hit count of this ruler which is used when clearing the 370 /// [rulerManager]'s cache to find the least used rulers. 371 void hit() { 372 _hitCount++; 373 } 374 375 /// Resets the hit count back to zero. 376 void resetHitCount() { 377 _hitCount = 0; 378 } 379 380 /// Makes sure this ruler is not used again after it has been disposed of, 381 /// which would indicate a bug. 382 @visibleForTesting 383 bool get debugIsDisposed => _debugIsDisposed; 384 bool _debugIsDisposed = false; 385 386 void _configureSingleLineHostElements() { 387 _singleLineHost.style 388 ..visibility = 'hidden' 389 ..position = 'absolute' 390 ..top = '0' // this is important as baseline == probe.bottom 391 ..left = '0' 392 ..display = 'flex' 393 ..flexDirection = 'row' 394 ..alignItems = 'baseline' 395 ..margin = '0' 396 ..border = '0' 397 ..padding = '0'; 398 399 singleLineDimensions.applyStyle(style); 400 401 // Force single-line (even if wider than screen) and preserve whitespaces. 402 singleLineDimensions._element.style.whiteSpace = 'pre'; 403 404 singleLineDimensions.appendToHost(_singleLineHost); 405 rulerManager.addHostElement(_singleLineHost); 406 } 407 408 void _configureMinIntrinsicHostElements() { 409 // Configure min intrinsic host elements. 410 _minIntrinsicHost.style 411 ..visibility = 'hidden' 412 ..position = 'absolute' 413 ..top = '0' // this is important as baseline == probe.bottom 414 ..left = '0' 415 ..display = 'flex' 416 ..flexDirection = 'row' 417 ..margin = '0' 418 ..border = '0' 419 ..padding = '0'; 420 421 minIntrinsicDimensions.applyStyle(style); 422 423 // "flex: 0" causes the paragraph element to shrink horizontally, exposing 424 // its minimum intrinsic width. 425 minIntrinsicDimensions._element.style 426 ..flex = '0' 427 ..display = 'inline' 428 // Preserve whitespaces. 429 ..whiteSpace = 'pre-wrap'; 430 431 _minIntrinsicHost.append(minIntrinsicDimensions._element); 432 rulerManager.addHostElement(_minIntrinsicHost); 433 } 434 435 void _configureConstrainedHostElements() { 436 _constrainedHost.style 437 ..visibility = 'hidden' 438 ..position = 'absolute' 439 ..top = '0' // this is important as baseline == probe.bottom 440 ..left = '0' 441 ..display = 'flex' 442 ..flexDirection = 'row' 443 ..alignItems = 'baseline' 444 ..margin = '0' 445 ..border = '0' 446 ..padding = '0'; 447 448 constrainedDimensions.applyStyle(style); 449 final html.CssStyleDeclaration elementStyle = 450 constrainedDimensions._element.style; 451 elementStyle 452 ..display = 'block' 453 ..overflowWrap = 'break-word'; 454 455 // TODO(flutter_web): Implement the ellipsis overflow for multi-line text 456 // too. As a pre-requisite, we need to be able to programmatically find 457 // line breaks. 458 if (style.ellipsis == null) { 459 elementStyle.whiteSpace = 'pre-wrap'; 460 } else { 461 // The height measurement is affected by whether the text has the ellipsis 462 // overflow property or not. This is because when ellipsis is set, we may 463 // not render all the lines, but stop at the first line that overflows. 464 elementStyle 465 ..whiteSpace = 'pre' 466 ..overflow = 'hidden' 467 ..textOverflow = 'ellipsis'; 468 } 469 470 constrainedDimensions.appendToHost(_constrainedHost); 471 rulerManager.addHostElement(_constrainedHost); 472 } 473 474 void _configureLineHeightHostElements() { 475 _lineHeightHost.style 476 ..visibility = 'hidden' 477 ..position = 'absolute' 478 ..top = '0' 479 ..left = '0' 480 ..display = 'flex' 481 ..flexDirection = 'row' 482 ..alignItems = 'baseline' 483 ..margin = '0' 484 ..border = '0' 485 ..padding = '0'; 486 487 lineHeightDimensions.applyStyle(style); 488 489 // Force single-line (even if wider than screen) and preserve whitespaces. 490 lineHeightDimensions._element.style.whiteSpace = 'pre'; 491 492 // To measure line-height, all we need is a whitespace. 493 lineHeightDimensions.updateTextToSpace(); 494 495 lineHeightDimensions.appendToHost(_lineHeightHost); 496 rulerManager.addHostElement(_lineHeightHost); 497 } 498 499 /// The paragraph being measured. 500 EngineParagraph _paragraph; 501 502 /// Prepares this ruler for measuring the given [paragraph]. 503 /// 504 /// This method must be called before calling any of the `measure*` methods. 505 void willMeasure(EngineParagraph paragraph) { 506 assert(paragraph != null); 507 assert(() { 508 if (_paragraph != null) { 509 throw Exception( 510 'Attempted to reuse a $ParagraphRuler but it is currently ' 511 'measuring another paragraph ($_paragraph). It is possible that '); 512 } 513 return true; 514 }()); 515 assert(paragraph._debugHasSameRootStyle(style)); 516 _paragraph = paragraph; 517 } 518 519 /// Prepares all 3 measurements: 520 /// 1. single line. 521 /// 2. minimum intrinsic width. 522 /// 3. constrained. 523 void measureAll(ui.ParagraphConstraints constraints) { 524 measureAsSingleLine(); 525 measureMinIntrinsicWidth(); 526 measureWithConstraints(constraints); 527 } 528 529 /// Lays out the paragraph in a single line, giving it infinite amount of 530 /// horizontal space. 531 /// 532 /// Measures [width], [height], and [alphabeticBaseline]. 533 void measureAsSingleLine() { 534 assert(!_debugIsDisposed); 535 assert(_paragraph != null); 536 537 // HACK(mdebbar): TextField uses an empty string to measure the line height, 538 // which doesn't work. So we need to replace it with a whitespace. The 539 // correct fix would be to do line height and baseline measurements and 540 // cache them separately. 541 if (_paragraph._plainText == '') { 542 singleLineDimensions.updateTextToSpace(); 543 } else { 544 singleLineDimensions.updateText(_paragraph, style); 545 } 546 } 547 548 /// Lays out the paragraph inside a flex row and sets "flex: 0", which 549 /// squeezes the paragraph, forcing it to occupy minimum intrinsic width. 550 /// 551 /// Measures [width] and [height]. 552 void measureMinIntrinsicWidth() { 553 assert(!_debugIsDisposed); 554 assert(_paragraph != null); 555 556 minIntrinsicDimensions.updateText(_paragraph, style); 557 } 558 559 /// Lays out the paragraph giving it a width constraint. 560 /// 561 /// Measures [width], [height], and [alphabeticBaseline]. 562 void measureWithConstraints(ui.ParagraphConstraints constraints) { 563 assert(!_debugIsDisposed); 564 assert(_paragraph != null); 565 566 constrainedDimensions.updateText(_paragraph, style); 567 568 // The extra 0.5 is because sometimes the browser needs slightly more space 569 // than the size it reports back. When that happens the text may be wrap 570 // when we thought it didn't. 571 constrainedDimensions.updateWidth('${constraints.width + 0.5}px'); 572 } 573 574 /// Performs clean-up after a measurement is done, preparing this ruler for 575 /// a future reuse. 576 /// 577 /// Call this method immediately after calling `measure*` methods for a 578 /// particular [paragraph]. This ruler is not reusable until [didMeasure] is 579 /// called. 580 void didMeasure() { 581 assert(_paragraph != null); 582 // Remove any rich text we set during layout for the following reasons: 583 // - there won't be any text for the browser to lay out when we commit the 584 // current frame. 585 // - this keeps the cost of removing content together with the measurement 586 // in the profile. Otherwise, the cost of removing will be paid by a 587 // random next paragraph measured in the future, and make the performance 588 // profile hard to understand. 589 // 590 // We do not do this for plain text, because replacing plain text is more 591 // expensive than paying the cost of the DOM mutation to clean it. 592 if (_paragraph._plainText == null) { 593 domRenderer 594 ..clearDom(singleLineDimensions._element) 595 ..clearDom(minIntrinsicDimensions._element) 596 ..clearDom(constrainedDimensions._element); 597 } 598 _paragraph = null; 599 } 600 601 /// Performs stateless measurement of text boxes for a given range of text. 602 /// 603 /// This method doesn't depend on [willMeasure] and [didMeasure] lifecycle 604 /// methods. 605 List<ui.TextBox> measureBoxesForRange( 606 String plainText, 607 ui.ParagraphConstraints constraints, { 608 int start, 609 int end, 610 double alignOffset, 611 ui.TextDirection textDirection, 612 }) { 613 assert(!_debugIsDisposed); 614 assert(start >= 0 && start <= plainText.length); 615 assert(end >= 0 && end <= plainText.length); 616 assert(start <= end); 617 618 final String before = plainText.substring(0, start); 619 final String rangeText = plainText.substring(start, end); 620 final String after = plainText.substring(end); 621 622 final html.SpanElement rangeSpan = html.SpanElement()..text = rangeText; 623 624 // Setup the [ruler.constrainedDimensions] element to be used for measurement. 625 domRenderer.clearDom(constrainedDimensions._element); 626 constrainedDimensions._element 627 ..appendText(before) 628 ..append(rangeSpan) 629 ..appendText(after); 630 constrainedDimensions.updateWidth('${constraints.width}px'); 631 632 // Measure the rects of [rangeSpan]. 633 final List<html.Rectangle<num>> clientRects = rangeSpan.getClientRects(); 634 final List<ui.TextBox> boxes = <ui.TextBox>[]; 635 636 for (html.Rectangle<num> rect in clientRects) { 637 boxes.add(ui.TextBox.fromLTRBD( 638 rect.left + alignOffset, 639 rect.top, 640 rect.right + alignOffset, 641 rect.bottom, 642 textDirection, 643 )); 644 } 645 646 // Cleanup after measuring the boxes. 647 domRenderer.clearDom(constrainedDimensions._element); 648 return boxes; 649 } 650 651 /// Detaches this ruler from the DOM and makes it unusable for future 652 /// measurements. 653 /// 654 /// Disposed rulers should be garbage collected after calling this method. 655 void dispose() { 656 assert(() { 657 if (_paragraph != null) { 658 throw Exception('Attempted to dispose of a ruler in the middle of ' 659 'measurement. This is likely a bug in the framework.'); 660 } 661 return true; 662 }()); 663 _singleLineHost.remove(); 664 _minIntrinsicHost.remove(); 665 _constrainedHost.remove(); 666 _lineHeightHost?.remove(); 667 assert(() { 668 _debugIsDisposed = true; 669 return true; 670 }()); 671 } 672 673 // Bounded cache for text measurement for a particular width constraint. 674 Map<String, List<MeasurementResult>> _measurementCache = 675 <String, List<MeasurementResult>>{}; 676 // Mru list for cache. 677 final List<String> _mruList = <String>[]; 678 static const int _cacheLimit = 2400; 679 // Number of items to evict when cache limit is reached. 680 static const int _cacheBlockFactor = 100; 681 // Number of constraint results per unique text item. 682 // This limit prevents growth during animation where the size of a container 683 // is changing. 684 static const int _constraintCacheSize = 8; 685 686 void cacheMeasurement(EngineParagraph paragraph, MeasurementResult item) { 687 final String plainText = paragraph._plainText; 688 final List<MeasurementResult> constraintCache = 689 _measurementCache[plainText] ??= <MeasurementResult>[]; 690 constraintCache.add(item); 691 if (constraintCache.length > _constraintCacheSize) { 692 constraintCache.removeAt(0); 693 } 694 _mruList.add(plainText); 695 if (_mruList.length > _cacheLimit) { 696 // Evict a range. 697 for (int i = 0; i < _cacheBlockFactor; i++) { 698 _measurementCache.remove(_mruList[i]); 699 } 700 _mruList.removeRange(0, _cacheBlockFactor); 701 } 702 } 703 704 MeasurementResult cacheLookup( 705 EngineParagraph paragraph, ui.ParagraphConstraints constraints) { 706 final List<MeasurementResult> constraintCache = 707 _measurementCache[paragraph._plainText]; 708 if (constraintCache == null) { 709 return null; 710 } 711 final int len = constraintCache.length; 712 for (int i = 0; i < len; i++) { 713 final MeasurementResult item = constraintCache[i]; 714 if (item.constraintWidth == constraints.width) { 715 return item; 716 } 717 } 718 return null; 719 } 720} 721 722/// The result that contains all measurements of a paragraph at the given 723/// constraint width. 724@immutable 725class MeasurementResult { 726 /// The width that was given as a constraint when the paragraph was laid out. 727 final double constraintWidth; 728 729 /// Whether the paragraph can fit in a single line given [constraintWidth]. 730 final bool isSingleLine; 731 732 /// The amount of horizontal space the paragraph occupies. 733 final double width; 734 735 /// The amount of vertical space the paragraph occupies. 736 final double height; 737 738 /// {@macro dart.ui.paragraph.naturalHeight} 739 /// 740 /// When [ParagraphGeometricStyle.maxLines] is null, [naturalHeight] and 741 /// [height] should be equal. 742 final double naturalHeight; 743 744 /// The amount of vertical space each line of the paragraph occupies. 745 /// 746 /// In some cases, measuring [lineHeight] is unnecessary, so it's nullable. If 747 /// present, it should be equal to [height] when [isSingleLine] is true. 748 final double lineHeight; 749 750 /// {@macro dart.ui.paragraph.minIntrinsicWidth} 751 final double minIntrinsicWidth; 752 753 /// {@macro dart.ui.paragraph.maxIntrinsicWidth} 754 final double maxIntrinsicWidth; 755 756 /// {@macro dart.ui.paragraph.alphabeticBaseline} 757 final double alphabeticBaseline; 758 759 /// {@macro dart.ui.paragraph.ideographicBaseline} 760 final double ideographicBaseline; 761 762 /// Substrings that represent how the text should wrap into multiple lines to 763 /// satisfy [constraintWidth], 764 final List<String> lines; 765 766 const MeasurementResult( 767 this.constraintWidth, { 768 @required this.isSingleLine, 769 @required this.width, 770 @required this.height, 771 @required this.naturalHeight, 772 @required this.lineHeight, 773 @required this.minIntrinsicWidth, 774 @required this.maxIntrinsicWidth, 775 @required this.alphabeticBaseline, 776 @required this.ideographicBaseline, 777 @required this.lines, 778 }) : assert(constraintWidth != null), 779 assert(isSingleLine != null), 780 assert(width != null), 781 assert(height != null), 782 assert(naturalHeight != null), 783 assert(minIntrinsicWidth != null), 784 assert(maxIntrinsicWidth != null), 785 assert(alphabeticBaseline != null), 786 assert(ideographicBaseline != null); 787} 788