• 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/// 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