1// Copyright 2014 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
5/// @docImport 'package:flutter/widgets.dart';
6library;
7
8import 'dart:math' show max;
9import 'dart:ui'
10 as ui
11 show
12 BoxHeightStyle,
13 BoxWidthStyle,
14 GlyphInfo,
15 LineMetrics,
16 Paragraph,
17 ParagraphBuilder,
18 ParagraphConstraints,
19 ParagraphStyle,
20 PlaceholderAlignment,
21 TextStyle;
22
23import 'package:flutter/foundation.dart';
24import 'package:flutter/services.dart';
25
26import 'basic_types.dart';
27import 'inline_span.dart';
28import 'placeholder_span.dart';
29import 'strut_style.dart';
30import 'text_scaler.dart';
31import 'text_span.dart';
32import 'text_style.dart';
33
34export 'dart:ui' show LineMetrics;
35export 'package:flutter/services.dart' show TextRange, TextSelection;
36
37/// The default font size if none is specified.
38///
39/// This should be kept in sync with the defaults set in the engine (e.g.,
40/// LibTxt's text_style.h, paragraph_style.h).
41const double kDefaultFontSize = 14.0;
42
43/// How overflowing text should be handled.
44///
45/// A [TextOverflow] can be passed to [Text] and [RichText] via their
46/// [Text.overflow] and [RichText.overflow] properties respectively.
47enum TextOverflow {
48 /// Clip the overflowing text to fix its container.
49 clip,
50
51 /// Fade the overflowing text to transparent.
52 fade,
53
54 /// Use an ellipsis to indicate that the text has overflowed.
55 ellipsis,
56
57 /// Render overflowing text outside of its container.
58 visible,
59}
60
61/// Holds the [Size] and baseline required to represent the dimensions of
62/// a placeholder in text.
63///
64/// Placeholders specify an empty space in the text layout, which is used
65/// to later render arbitrary inline widgets into defined by a [WidgetSpan].
66///
67/// See also:
68///
69/// * [WidgetSpan], a subclass of [InlineSpan] and [PlaceholderSpan] that
70/// represents an inline widget embedded within text. The space this
71/// widget takes is indicated by a placeholder.
72/// * [RichText], a text widget that supports text inline widgets.
73@immutable
74class PlaceholderDimensions {
75 /// Constructs a [PlaceholderDimensions] with the specified parameters.
76 ///
77 /// The `size` and `alignment` are required as a placeholder's dimensions
78 /// require at least `size` and `alignment` to be fully defined.
79 const PlaceholderDimensions({
80 required this.size,
81 required this.alignment,
82 this.baseline,
83 this.baselineOffset,
84 });
85
86 /// A constant representing an empty placeholder.
87 static const PlaceholderDimensions empty = PlaceholderDimensions(
88 size: Size.zero,
89 alignment: ui.PlaceholderAlignment.bottom,
90 );
91
92 /// Width and height dimensions of the placeholder.
93 final Size size;
94
95 /// How to align the placeholder with the text.
96 ///
97 /// See also:
98 ///
99 /// * [baseline], the baseline to align to when using
100 /// [dart:ui.PlaceholderAlignment.baseline],
101 /// [dart:ui.PlaceholderAlignment.aboveBaseline],
102 /// or [dart:ui.PlaceholderAlignment.belowBaseline].
103 /// * [baselineOffset], the distance of the alphabetic baseline from the upper
104 /// edge of the placeholder.
105 final ui.PlaceholderAlignment alignment;
106
107 /// Distance of the [baseline] from the upper edge of the placeholder.
108 ///
109 /// Only used when [alignment] is [ui.PlaceholderAlignment.baseline].
110 final double? baselineOffset;
111
112 /// The [TextBaseline] to align to. Used with:
113 ///
114 /// * [ui.PlaceholderAlignment.baseline]
115 /// * [ui.PlaceholderAlignment.aboveBaseline]
116 /// * [ui.PlaceholderAlignment.belowBaseline]
117 /// * [ui.PlaceholderAlignment.middle]
118 final TextBaseline? baseline;
119
120 @override
121 bool operator ==(Object other) {
122 if (identical(this, other)) {
123 return true;
124 }
125 return other is PlaceholderDimensions &&
126 other.size == size &&
127 other.alignment == alignment &&
128 other.baseline == baseline &&
129 other.baselineOffset == baselineOffset;
130 }
131
132 @override
133 int get hashCode => Object.hash(size, alignment, baseline, baselineOffset);
134
135 @override
136 String toString() {
137 return switch (alignment) {
138 ui.PlaceholderAlignment.top ||
139 ui.PlaceholderAlignment.bottom ||
140 ui.PlaceholderAlignment.middle ||
141 ui.PlaceholderAlignment.aboveBaseline ||
142 ui.PlaceholderAlignment.belowBaseline => 'PlaceholderDimensions($size, $alignment)',
143 ui.PlaceholderAlignment.baseline =>
144 'PlaceholderDimensions($size, $alignment($baselineOffset from top))',
145 };
146 }
147}
148
149/// The different ways of measuring the width of one or more lines of text.
150///
151/// See [Text.textWidthBasis], for example.
152enum TextWidthBasis {
153 /// multiline text will take up the full width given by the parent. For single
154 /// line text, only the minimum amount of width needed to contain the text
155 /// will be used. A common use case for this is a standard series of
156 /// paragraphs.
157 parent,
158
159 /// The width will be exactly enough to contain the longest line and no
160 /// longer. A common use case for this is chat bubbles.
161 longestLine,
162}
163
164/// A [TextBoundary] subclass for locating word breaks.
165///
166/// The underlying implementation uses [UAX #29](https://unicode.org/reports/tr29/)
167/// defined default word boundaries.
168///
169/// The default word break rules can be tailored to meet the requirements of
170/// different use cases. For instance, the default rule set keeps horizontal
171/// whitespaces together as a single word, which may not make sense in a
172/// word-counting context -- "hello world" counts as 3 words instead of 2.
173/// An example is the [moveByWordBoundary] variant, which is a tailored
174/// word-break locator that more closely matches the default behavior of most
175/// platforms and editors when it comes to handling text editing keyboard
176/// shortcuts that move or delete word by word.
177class WordBoundary extends TextBoundary {
178 /// Creates a [WordBoundary] with the text and layout information.
179 WordBoundary._(this._text, this._paragraph);
180
181 final InlineSpan _text;
182 final ui.Paragraph _paragraph;
183
184 @override
185 TextRange getTextBoundaryAt(int position) =>
186 _paragraph.getWordBoundary(TextPosition(offset: max(position, 0)));
187
188 // Combines two UTF-16 code units (high surrogate + low surrogate) into a
189 // single code point that represents a supplementary character.
190 static int _codePointFromSurrogates(int highSurrogate, int lowSurrogate) {
191 assert(
192 TextPainter.isHighSurrogate(highSurrogate),
193 'U+${highSurrogate.toRadixString(16).toUpperCase().padLeft(4, "0")}) is not a high surrogate.',
194 );
195 assert(
196 TextPainter.isLowSurrogate(lowSurrogate),
197 'U+${lowSurrogate.toRadixString(16).toUpperCase().padLeft(4, "0")}) is not a low surrogate.',
198 );
199 const int base = 0x010000 - (0xD800 << 10) - 0xDC00;
200 return (highSurrogate << 10) + lowSurrogate + base;
201 }
202
203 // The Runes class does not provide random access with a code unit offset.
204 int? _codePointAt(int index) {
205 final int? codeUnitAtIndex = _text.codeUnitAt(index);
206 if (codeUnitAtIndex == null) {
207 return null;
208 }
209 return switch (codeUnitAtIndex & 0xFC00) {
210 0xD800 => _codePointFromSurrogates(codeUnitAtIndex, _text.codeUnitAt(index + 1)!),
211 0xDC00 => _codePointFromSurrogates(_text.codeUnitAt(index - 1)!, codeUnitAtIndex),
212 _ => codeUnitAtIndex,
213 };
214 }
215
216 static bool _isNewline(int codePoint) {
217 // Carriage Return is not treated as a hard line break.
218 return switch (codePoint) {
219 0x000A || // Line Feed
220 0x0085 || // New Line
221 0x000B || // Form Feed
222 0x000C || // Vertical Feed
223 0x2028 || // Line Separator
224 0x2029 => true, // Paragraph Separator
225 _ => false,
226 };
227 }
228
229 static final RegExp _regExpSpaceSeparatorOrPunctuation = RegExp(
230 r'[\p{Space_Separator}\p{Punctuation}]',
231 unicode: true,
232 );
233 bool _skipSpacesAndPunctuations(int offset, bool forward) {
234 // Use code point since some punctuations are supplementary characters.
235 // "inner" here refers to the code unit that's before the break in the
236 // search direction (`forward`).
237 final int? innerCodePoint = _codePointAt(forward ? offset - 1 : offset);
238 final int? outerCodeUnit = _text.codeUnitAt(forward ? offset : offset - 1);
239
240 // Make sure the hard break rules in UAX#29 take precedence over the ones we
241 // add below. Luckily there're only 4 hard break rules for word breaks, and
242 // dictionary based breaking does not introduce new hard breaks:
243 // https://unicode-org.github.io/icu/userguide/boundaryanalysis/break-rules.html#word-dictionaries
244 //
245 // WB1 & WB2: always break at the start or the end of the text.
246 final bool hardBreakRulesApply =
247 innerCodePoint == null ||
248 outerCodeUnit == null
249 // WB3a & WB3b: always break before and after newlines.
250 ||
251 _isNewline(innerCodePoint) ||
252 _isNewline(outerCodeUnit);
253 return hardBreakRulesApply ||
254 !_regExpSpaceSeparatorOrPunctuation.hasMatch(String.fromCharCode(innerCodePoint));
255 }
256
257 /// Returns a [TextBoundary] suitable for handling keyboard navigation
258 /// commands that change the current selection word by word.
259 ///
260 /// This [TextBoundary] is used by text widgets in the flutter framework to
261 /// provide default implementation for text editing shortcuts, for example,
262 /// "delete to the previous word".
263 ///
264 /// The implementation applies the same set of rules [WordBoundary] uses,
265 /// except that word breaks end on a space separator or a punctuation will be
266 /// skipped, to match the behavior of most platforms. Additional rules may be
267 /// added in the future to better match platform behaviors.
268 late final TextBoundary moveByWordBoundary = _UntilTextBoundary(this, _skipSpacesAndPunctuations);
269}
270
271class _UntilTextBoundary extends TextBoundary {
272 const _UntilTextBoundary(this._textBoundary, this._predicate);
273
274 final UntilPredicate _predicate;
275 final TextBoundary _textBoundary;
276
277 @override
278 int? getLeadingTextBoundaryAt(int position) {
279 if (position < 0) {
280 return null;
281 }
282 final int? offset = _textBoundary.getLeadingTextBoundaryAt(position);
283 return offset == null || _predicate(offset, false)
284 ? offset
285 : getLeadingTextBoundaryAt(offset - 1);
286 }
287
288 @override
289 int? getTrailingTextBoundaryAt(int position) {
290 final int? offset = _textBoundary.getTrailingTextBoundaryAt(max(position, 0));
291 return offset == null || _predicate(offset, true) ? offset : getTrailingTextBoundaryAt(offset);
292 }
293}
294
295class _TextLayout {
296 _TextLayout._(this._paragraph, this.writingDirection, this._painter);
297
298 final TextDirection writingDirection;
299
300 // Computing plainText is a bit expensive and is currently not needed for
301 // simple static text. Pass in the entire text painter so `TextPainter.plainText`
302 // is only called when needed.
303 final TextPainter _painter;
304
305 // This field is not final because the owner TextPainter could create a new
306 // ui.Paragraph with the exact same text layout (for example, when only the
307 // color of the text is changed).
308 //
309 // The creator of this _TextLayout is also responsible for disposing this
310 // object when it's no longer needed.
311 ui.Paragraph _paragraph;
312
313 /// Whether this layout has been invalidated and disposed.
314 ///
315 /// Only for use when asserts are enabled.
316 bool get debugDisposed => _paragraph.debugDisposed;
317
318 /// The horizontal space required to paint this text.
319 ///
320 /// If a line ends with trailing spaces, the trailing spaces may extend
321 /// outside of the horizontal paint bounds defined by [width].
322 double get width => _paragraph.width;
323
324 /// The vertical space required to paint this text.
325 double get height => _paragraph.height;
326
327 /// The width at which decreasing the width of the text would prevent it from
328 /// painting itself completely within its bounds.
329 double get minIntrinsicLineExtent => _paragraph.minIntrinsicWidth;
330
331 /// The width at which increasing the width of the text no longer decreases the height.
332 ///
333 /// Includes trailing spaces if any.
334 double get maxIntrinsicLineExtent => _paragraph.maxIntrinsicWidth;
335
336 /// The distance from the left edge of the leftmost glyph to the right edge of
337 /// the rightmost glyph in the paragraph.
338 double get longestLine => _paragraph.longestLine;
339
340 /// Returns the distance from the top of the text to the first baseline of the
341 /// given type.
342 double getDistanceToBaseline(TextBaseline baseline) {
343 return switch (baseline) {
344 TextBaseline.alphabetic => _paragraph.alphabeticBaseline,
345 TextBaseline.ideographic => _paragraph.ideographicBaseline,
346 };
347 }
348
349 static final RegExp _regExpSpaceSeparators = RegExp(r'\p{Space_Separator}', unicode: true);
350
351 /// The line caret metrics representing the end of text location.
352 ///
353 /// This is usually used when the caret is placed at the end of the text
354 /// (text.length, downstream), unless maxLines is set to a non-null value, in
355 /// which case the caret is placed at the visual end of the last visible line.
356 ///
357 /// This should not be called when the paragraph is empty as the implementation
358 /// relies on line metrics.
359 ///
360 /// When the last bidi level run in the paragraph and the paragraph's bidi
361 /// levels have opposite parities (which implies opposite writing directions),
362 /// this makes sure the caret is placed at the same "end" of the line as if the
363 /// line ended with a line feed.
364 late final _LineCaretMetrics _endOfTextCaretMetrics = _computeEndOfTextCaretAnchorOffset();
365 _LineCaretMetrics _computeEndOfTextCaretAnchorOffset() {
366 final String rawString = _painter.plainText;
367 final int lastLineIndex = _paragraph.numberOfLines - 1;
368 assert(lastLineIndex >= 0);
369 final ui.LineMetrics lineMetrics = _paragraph.getLineMetricsAt(lastLineIndex)!;
370 // Trailing white spaces don't contribute to the line width and thus require special handling
371 // when they're present.
372 // Luckily they have the same bidi embedding level as the paragraph as per
373 // https://unicode.org/reports/tr9/#L1, so we can anchor the caret to the
374 // last logical trailing space.
375 // Whitespace character definitions refer to Java/ICU, not Unicode-Zs.
376 // https://github.com/unicode-org/icu/blob/23d9628f88a2d0127c564ad98297061c36d3ce77/icu4c/source/common/unicode/uchar.h#L3388-L3425
377 final String lastCodeUnit = rawString[rawString.length - 1];
378 final bool hasTrailingSpaces = switch (lastCodeUnit.codeUnitAt(0)) {
379 0x0009 => true, // horizontal tab
380 0x00A0 || // no-break space
381 0x2007 || // figure space
382 0x202F => false, // narrow no-break space
383 _ => _regExpSpaceSeparators.hasMatch(lastCodeUnit),
384 };
385
386 final double baseline = lineMetrics.baseline;
387 final double dx;
388 final double height;
389 late final ui.GlyphInfo? lastGlyph = _paragraph.getGlyphInfoAt(rawString.length - 1);
390 // TODO(LongCatIsLooong): handle the case where maxLine is set to non-null
391 // and the last line ends with trailing whitespaces.
392 if (hasTrailingSpaces && lastGlyph != null) {
393 final Rect glyphBounds = lastGlyph.graphemeClusterLayoutBounds;
394 assert(!glyphBounds.isEmpty);
395 dx = switch (writingDirection) {
396 TextDirection.ltr => glyphBounds.right,
397 TextDirection.rtl => glyphBounds.left,
398 };
399 height = glyphBounds.height;
400 } else {
401 dx = switch (writingDirection) {
402 TextDirection.ltr => lineMetrics.left + lineMetrics.width,
403 TextDirection.rtl => lineMetrics.left,
404 };
405 height = lineMetrics.height;
406 }
407 return _LineCaretMetrics(
408 offset: Offset(dx, baseline),
409 writingDirection: writingDirection,
410 height: height,
411 );
412 }
413
414 double _contentWidthFor(double minWidth, double maxWidth, TextWidthBasis widthBasis) {
415 return switch (widthBasis) {
416 TextWidthBasis.longestLine => clampDouble(longestLine, minWidth, maxWidth),
417 TextWidthBasis.parent => clampDouble(maxIntrinsicLineExtent, minWidth, maxWidth),
418 };
419 }
420}
421
422// This class stores the current text layout and the corresponding
423// paintOffset/contentWidth, as well as some cached text metrics values that
424// depends on the current text layout, which will be invalidated as soon as the
425// text layout is invalidated.
426class _TextPainterLayoutCacheWithOffset {
427 _TextPainterLayoutCacheWithOffset(
428 this.layout,
429 this.textAlignment,
430 this.layoutMaxWidth,
431 this.contentWidth,
432 ) : assert(textAlignment >= 0.0 && textAlignment <= 1.0),
433 assert(!layoutMaxWidth.isNaN),
434 assert(!contentWidth.isNaN);
435
436 final _TextLayout layout;
437
438 // The input width used to lay out the paragraph.
439 final double layoutMaxWidth;
440
441 // The content width the text painter should report in TextPainter.width.
442 // This is also used to compute `paintOffset`.
443 double contentWidth;
444
445 // The effective text alignment in the TextPainter's canvas. The value is
446 // within the [0, 1] interval: 0 for left aligned and 1 for right aligned.
447 final double textAlignment;
448
449 // The paintOffset of the `paragraph` in the TextPainter's canvas.
450 //
451 // It's coordinate values are guaranteed to not be NaN.
452 Offset get paintOffset {
453 if (textAlignment == 0) {
454 return Offset.zero;
455 }
456 if (!paragraph.width.isFinite) {
457 return const Offset(double.infinity, 0.0);
458 }
459 final double dx = textAlignment * (contentWidth - paragraph.width);
460 assert(!dx.isNaN);
461 return Offset(dx, 0);
462 }
463
464 ui.Paragraph get paragraph => layout._paragraph;
465
466 // Try to resize the contentWidth to fit the new input constraints, by just
467 // adjusting the paint offset (so no line-breaking changes needed).
468 //
469 // Returns false if the new constraints require the text layout library to
470 // re-compute the line breaks.
471 bool _resizeToFit(double minWidth, double maxWidth, TextWidthBasis widthBasis) {
472 assert(layout.maxIntrinsicLineExtent.isFinite);
473 assert(minWidth <= maxWidth);
474 // The assumption here is that if a Paragraph's width is already >= its
475 // maxIntrinsicWidth, further increasing the input width does not change its
476 // layout (but may change the paint offset if it's not left-aligned). This is
477 // true even for TextAlign.justify: when width >= maxIntrinsicWidth
478 // TextAlign.justify will behave exactly the same as TextAlign.start.
479 //
480 // An exception to this is when the text is not left-aligned, and the input
481 // width is double.infinity. Since the resulting Paragraph will have a width
482 // of double.infinity, and to make the text visible the paintOffset.dx is
483 // bound to be double.negativeInfinity, which invalidates all arithmetic
484 // operations.
485
486 if (maxWidth == contentWidth && minWidth == contentWidth) {
487 contentWidth = layout._contentWidthFor(minWidth, maxWidth, widthBasis);
488 return true;
489 }
490
491 // Special case:
492 // When the paint offset and the paragraph width are both +∞, it's likely
493 // that the text layout engine skipped layout because there weren't anything
494 // to paint. Always try to re-compute the text layout.
495 if (!paintOffset.dx.isFinite && !paragraph.width.isFinite && minWidth.isFinite) {
496 assert(paintOffset.dx == double.infinity);
497 assert(paragraph.width == double.infinity);
498 return false;
499 }
500
501 final double maxIntrinsicWidth = paragraph.maxIntrinsicWidth;
502 // Skip line breaking if the input width remains the same, of there will be
503 // no soft breaks.
504 final bool skipLineBreaking =
505 maxWidth ==
506 layoutMaxWidth // Same input max width so relayout is unnecessary.
507 ||
508 ((paragraph.width - maxIntrinsicWidth) > -precisionErrorTolerance &&
509 (maxWidth - maxIntrinsicWidth) > -precisionErrorTolerance);
510 if (skipLineBreaking) {
511 // Adjust the content width in case the TextWidthBasis changed.
512 contentWidth = layout._contentWidthFor(minWidth, maxWidth, widthBasis);
513 return true;
514 }
515 return false;
516 }
517
518 // ---- Cached Values ----
519
520 List<TextBox> get inlinePlaceholderBoxes =>
521 _cachedInlinePlaceholderBoxes ??= paragraph.getBoxesForPlaceholders();
522 List<TextBox>? _cachedInlinePlaceholderBoxes;
523
524 List<ui.LineMetrics> get lineMetrics => _cachedLineMetrics ??= paragraph.computeLineMetrics();
525 List<ui.LineMetrics>? _cachedLineMetrics;
526
527 // Used to determine whether the caret metrics cache should be invalidated.
528 int? _previousCaretPositionKey;
529}
530
531/// The _CaretMetrics for carets located in a non-empty paragraph. Such carets
532/// are anchored to the trailing edge or the leading edge of a glyph, or a
533/// ligature component.
534class _LineCaretMetrics {
535 const _LineCaretMetrics({
536 required this.offset,
537 required this.writingDirection,
538 required this.height,
539 });
540
541 /// The offset from the top left corner of the paragraph to the caret's top
542 /// start location.
543 final Offset offset;
544
545 /// The writing direction of the glyph the _LineCaretMetrics is associated with.
546 /// The value determines whether the cursor is painted to the left or to the
547 /// right of [offset].
548 final TextDirection writingDirection;
549
550 /// The recommended height of the caret.
551 final double height;
552
553 _LineCaretMetrics shift(Offset offset) {
554 return offset == Offset.zero
555 ? this
556 : _LineCaretMetrics(
557 offset: offset + this.offset,
558 writingDirection: writingDirection,
559 height: height,
560 );
561 }
562}
563
564/// An object that paints a [TextSpan] tree into a [Canvas].
565///
566/// To use a [TextPainter], follow these steps:
567///
568/// 1. Create a [TextSpan] tree and pass it to the [TextPainter]
569/// constructor.
570///
571/// 2. Call [layout] to prepare the paragraph.
572///
573/// 3. Call [paint] as often as desired to paint the paragraph.
574///
575/// 4. Call [dispose] when the object will no longer be accessed to release
576/// native resources. For [TextPainter] objects that are used repeatedly and
577/// stored on a [State] or [RenderObject], call [dispose] from
578/// [State.dispose] or [RenderObject.dispose] or similar. For [TextPainter]
579/// objects that are only used ephemerally, it is safe to immediately dispose
580/// them after the last call to methods or properties on the object.
581///
582/// If the width of the area into which the text is being painted
583/// changes, return to step 2. If the text to be painted changes,
584/// return to step 1.
585///
586/// The default text style color is white on non-web platforms and black on
587/// the web. If developing across both platforms, always set the text color
588/// explicitly.
589class TextPainter {
590 /// Creates a text painter that paints the given text.
591 ///
592 /// The `text` and `textDirection` arguments are optional but [text] and
593 /// [textDirection] must be non-null before calling [layout].
594 ///
595 /// The [maxLines] property, if non-null, must be greater than zero.
596 TextPainter({
597 InlineSpan? text,
598 TextAlign textAlign = TextAlign.start,
599 TextDirection? textDirection,
600 @Deprecated(
601 'Use textScaler instead. '
602 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
603 'This feature was deprecated after v3.12.0-2.0.pre.',
604 )
605 double textScaleFactor = 1.0,
606 TextScaler textScaler = const _UnspecifiedTextScaler(),
607 int? maxLines,
608 String? ellipsis,
609 Locale? locale,
610 StrutStyle? strutStyle,
611 TextWidthBasis textWidthBasis = TextWidthBasis.parent,
612 TextHeightBehavior? textHeightBehavior,
613 }) : assert(text == null || text.debugAssertIsValid()),
614 assert(maxLines == null || maxLines > 0),
615 assert(
616 textScaleFactor == 1.0 || identical(textScaler, const _UnspecifiedTextScaler()),
617 'Use textScaler instead.',
618 ),
619 _text = text,
620 _textAlign = textAlign,
621 _textDirection = textDirection,
622 _textScaler = textScaler == const _UnspecifiedTextScaler()
623 ? TextScaler.linear(textScaleFactor)
624 : textScaler,
625 _maxLines = maxLines,
626 _ellipsis = ellipsis,
627 _locale = locale,
628 _strutStyle = strutStyle,
629 _textWidthBasis = textWidthBasis,
630 _textHeightBehavior = textHeightBehavior {
631 assert(debugMaybeDispatchCreated('painting', 'TextPainter', this));
632 }
633
634 /// Computes the width of a configured [TextPainter].
635 ///
636 /// This is a convenience method that creates a text painter with the supplied
637 /// parameters, lays it out with the supplied [minWidth] and [maxWidth], and
638 /// returns its [TextPainter.width] making sure to dispose the underlying
639 /// resources. Doing this operation is expensive and should be avoided
640 /// whenever it is possible to preserve the [TextPainter] to paint the
641 /// text or get other information about it.
642 static double computeWidth({
643 required InlineSpan text,
644 required TextDirection textDirection,
645 TextAlign textAlign = TextAlign.start,
646 @Deprecated(
647 'Use textScaler instead. '
648 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
649 'This feature was deprecated after v3.12.0-2.0.pre.',
650 )
651 double textScaleFactor = 1.0,
652 TextScaler textScaler = TextScaler.noScaling,
653 int? maxLines,
654 String? ellipsis,
655 Locale? locale,
656 StrutStyle? strutStyle,
657 TextWidthBasis textWidthBasis = TextWidthBasis.parent,
658 TextHeightBehavior? textHeightBehavior,
659 double minWidth = 0.0,
660 double maxWidth = double.infinity,
661 }) {
662 assert(
663 textScaleFactor == 1.0 || identical(textScaler, TextScaler.noScaling),
664 'Use textScaler instead.',
665 );
666 final TextPainter painter = TextPainter(
667 text: text,
668 textAlign: textAlign,
669 textDirection: textDirection,
670 textScaler: textScaler == TextScaler.noScaling
671 ? TextScaler.linear(textScaleFactor)
672 : textScaler,
673 maxLines: maxLines,
674 ellipsis: ellipsis,
675 locale: locale,
676 strutStyle: strutStyle,
677 textWidthBasis: textWidthBasis,
678 textHeightBehavior: textHeightBehavior,
679 )..layout(minWidth: minWidth, maxWidth: maxWidth);
680
681 try {
682 return painter.width;
683 } finally {
684 painter.dispose();
685 }
686 }
687
688 /// Computes the max intrinsic width of a configured [TextPainter].
689 ///
690 /// This is a convenience method that creates a text painter with the supplied
691 /// parameters, lays it out with the supplied [minWidth] and [maxWidth], and
692 /// returns its [TextPainter.maxIntrinsicWidth] making sure to dispose the
693 /// underlying resources. Doing this operation is expensive and should be avoided
694 /// whenever it is possible to preserve the [TextPainter] to paint the
695 /// text or get other information about it.
696 static double computeMaxIntrinsicWidth({
697 required InlineSpan text,
698 required TextDirection textDirection,
699 TextAlign textAlign = TextAlign.start,
700 @Deprecated(
701 'Use textScaler instead. '
702 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
703 'This feature was deprecated after v3.12.0-2.0.pre.',
704 )
705 double textScaleFactor = 1.0,
706 TextScaler textScaler = TextScaler.noScaling,
707 int? maxLines,
708 String? ellipsis,
709 Locale? locale,
710 StrutStyle? strutStyle,
711 TextWidthBasis textWidthBasis = TextWidthBasis.parent,
712 TextHeightBehavior? textHeightBehavior,
713 double minWidth = 0.0,
714 double maxWidth = double.infinity,
715 }) {
716 assert(
717 textScaleFactor == 1.0 || identical(textScaler, TextScaler.noScaling),
718 'Use textScaler instead.',
719 );
720 final TextPainter painter = TextPainter(
721 text: text,
722 textAlign: textAlign,
723 textDirection: textDirection,
724 textScaler: textScaler == TextScaler.noScaling
725 ? TextScaler.linear(textScaleFactor)
726 : textScaler,
727 maxLines: maxLines,
728 ellipsis: ellipsis,
729 locale: locale,
730 strutStyle: strutStyle,
731 textWidthBasis: textWidthBasis,
732 textHeightBehavior: textHeightBehavior,
733 )..layout(minWidth: minWidth, maxWidth: maxWidth);
734
735 try {
736 return painter.maxIntrinsicWidth;
737 } finally {
738 painter.dispose();
739 }
740 }
741
742 // Whether textWidthBasis has changed after the most recent `layout` call.
743 bool _debugNeedsRelayout = true;
744 // The result of the most recent `layout` call.
745 _TextPainterLayoutCacheWithOffset? _layoutCache;
746
747 // Whether _layoutCache contains outdated paint information and needs to be
748 // updated before painting.
749 //
750 // ui.Paragraph is entirely immutable, thus text style changes that can affect
751 // layout and those who can't both require the ui.Paragraph object being
752 // recreated. The caller may not call `layout` again after text color is
753 // updated. See: https://github.com/flutter/flutter/issues/85108
754 bool _rebuildParagraphForPaint = true;
755
756 bool get _debugAssertTextLayoutIsValid {
757 assert(!debugDisposed);
758 if (_layoutCache == null) {
759 throw FlutterError.fromParts(<DiagnosticsNode>[
760 ErrorSummary('Text layout not available'),
761 if (_debugMarkNeedsLayoutCallStack != null)
762 DiagnosticsStackTrace(
763 'The calls that first invalidated the text layout were',
764 _debugMarkNeedsLayoutCallStack,
765 )
766 else
767 ErrorDescription('The TextPainter has never been laid out.'),
768 ]);
769 }
770 return true;
771 }
772
773 StackTrace? _debugMarkNeedsLayoutCallStack;
774
775 /// Marks this text painter's layout information as dirty and removes cached
776 /// information.
777 ///
778 /// Uses this method to notify text painter to relayout in the case of
779 /// layout changes in engine. In most cases, updating text painter properties
780 /// in framework will automatically invoke this method.
781 void markNeedsLayout() {
782 assert(() {
783 if (_layoutCache != null) {
784 _debugMarkNeedsLayoutCallStack ??= StackTrace.current;
785 }
786 return true;
787 }());
788 _layoutCache?.paragraph.dispose();
789 _layoutCache = null;
790 }
791
792 /// The (potentially styled) text to paint.
793 ///
794 /// After this is set, you must call [layout] before the next call to [paint].
795 /// This and [textDirection] must be non-null before you call [layout].
796 ///
797 /// The [InlineSpan] this provides is in the form of a tree that may contain
798 /// multiple instances of [TextSpan]s and [WidgetSpan]s. To obtain a plain text
799 /// representation of the contents of this [TextPainter], use [plainText].
800 InlineSpan? get text => _text;
801 InlineSpan? _text;
802 set text(InlineSpan? value) {
803 assert(value == null || value.debugAssertIsValid());
804 if (_text == value) {
805 return;
806 }
807 if (_text?.style != value?.style) {
808 _layoutTemplate?.dispose();
809 _layoutTemplate = null;
810 }
811
812 final RenderComparison comparison = value == null
813 ? RenderComparison.layout
814 : _text?.compareTo(value) ?? RenderComparison.layout;
815
816 _text = value;
817 _cachedPlainText = null;
818
819 if (comparison.index >= RenderComparison.layout.index) {
820 markNeedsLayout();
821 } else if (comparison.index >= RenderComparison.paint.index) {
822 // Don't invalid the _layoutCache just yet. It still contains valid layout
823 // information.
824 _rebuildParagraphForPaint = true;
825 }
826 // Neither relayout or repaint is needed.
827 }
828
829 /// Returns a plain text version of the text to paint.
830 ///
831 /// This uses [InlineSpan.toPlainText] to get the full contents of all nodes in the tree.
832 String get plainText {
833 _cachedPlainText ??= _text?.toPlainText(includeSemanticsLabels: false);
834 return _cachedPlainText ?? '';
835 }
836
837 String? _cachedPlainText;
838
839 /// How the text should be aligned horizontally.
840 ///
841 /// After this is set, you must call [layout] before the next call to [paint].
842 ///
843 /// The [textAlign] property defaults to [TextAlign.start].
844 TextAlign get textAlign => _textAlign;
845 TextAlign _textAlign;
846 set textAlign(TextAlign value) {
847 if (_textAlign == value) {
848 return;
849 }
850 _textAlign = value;
851 markNeedsLayout();
852 }
853
854 /// The default directionality of the text.
855 ///
856 /// This controls how the [TextAlign.start], [TextAlign.end], and
857 /// [TextAlign.justify] values of [textAlign] are resolved.
858 ///
859 /// This is also used to disambiguate how to render bidirectional text. For
860 /// example, if the [text] is an English phrase followed by a Hebrew phrase,
861 /// in a [TextDirection.ltr] context the English phrase will be on the left
862 /// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
863 /// context, the English phrase will be on the right and the Hebrew phrase on
864 /// its left.
865 ///
866 /// After this is set, you must call [layout] before the next call to [paint].
867 ///
868 /// This and [text] must be non-null before you call [layout].
869 TextDirection? get textDirection => _textDirection;
870 TextDirection? _textDirection;
871 set textDirection(TextDirection? value) {
872 if (_textDirection == value) {
873 return;
874 }
875 _textDirection = value;
876 markNeedsLayout();
877 _layoutTemplate?.dispose();
878 _layoutTemplate = null; // Shouldn't really matter, but for strict correctness...
879 }
880
881 /// Deprecated. Will be removed in a future version of Flutter. Use
882 /// [textScaler] instead.
883 ///
884 /// The number of font pixels for each logical pixel.
885 ///
886 /// For example, if the text scale factor is 1.5, text will be 50% larger than
887 /// the specified font size.
888 ///
889 /// After this is set, you must call [layout] before the next call to [paint].
890 @Deprecated(
891 'Use textScaler instead. '
892 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
893 'This feature was deprecated after v3.12.0-2.0.pre.',
894 )
895 double get textScaleFactor => textScaler.textScaleFactor;
896 @Deprecated(
897 'Use textScaler instead. '
898 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
899 'This feature was deprecated after v3.12.0-2.0.pre.',
900 )
901 set textScaleFactor(double value) {
902 textScaler = TextScaler.linear(value);
903 }
904
905 /// {@template flutter.painting.textPainter.textScaler}
906 /// The font scaling strategy to use when laying out and rendering the text.
907 ///
908 /// The value usually comes from [MediaQuery.textScalerOf], which typically
909 /// reflects the user-specified text scaling value in the platform's
910 /// accessibility settings. The [TextStyle.fontSize] of the text will be
911 /// adjusted by the [TextScaler] before the text is laid out and rendered.
912 /// {@endtemplate}
913 ///
914 /// The [layout] method must be called after [textScaler] changes as it
915 /// affects the text layout.
916 TextScaler get textScaler => _textScaler;
917 TextScaler _textScaler;
918 set textScaler(TextScaler value) {
919 if (value == _textScaler) {
920 return;
921 }
922 _textScaler = value;
923 markNeedsLayout();
924 _layoutTemplate?.dispose();
925 _layoutTemplate = null;
926 }
927
928 /// The string used to ellipsize overflowing text. Setting this to a non-empty
929 /// string will cause this string to be substituted for the remaining text
930 /// if the text can not fit within the specified maximum width.
931 ///
932 /// Specifically, the ellipsis is applied to the last line before the line
933 /// truncated by [maxLines], if [maxLines] is non-null and that line overflows
934 /// the width constraint, or to the first line that is wider than the width
935 /// constraint, if [maxLines] is null. The width constraint is the `maxWidth`
936 /// passed to [layout].
937 ///
938 /// After this is set, you must call [layout] before the next call to [paint].
939 ///
940 /// The higher layers of the system, such as the [Text] widget, represent
941 /// overflow effects using the [TextOverflow] enum. The
942 /// [TextOverflow.ellipsis] value corresponds to setting this property to
943 /// U+2026 HORIZONTAL ELLIPSIS (…).
944 String? get ellipsis => _ellipsis;
945 String? _ellipsis;
946 set ellipsis(String? value) {
947 assert(value == null || value.isNotEmpty);
948 if (_ellipsis == value) {
949 return;
950 }
951 _ellipsis = value;
952 markNeedsLayout();
953 }
954
955 /// The locale used to select region-specific glyphs.
956 Locale? get locale => _locale;
957 Locale? _locale;
958 set locale(Locale? value) {
959 if (_locale == value) {
960 return;
961 }
962 _locale = value;
963 markNeedsLayout();
964 }
965
966 /// An optional maximum number of lines for the text to span, wrapping if
967 /// necessary.
968 ///
969 /// If the text exceeds the given number of lines, it is truncated such that
970 /// subsequent lines are dropped.
971 ///
972 /// After this is set, you must call [layout] before the next call to [paint].
973 int? get maxLines => _maxLines;
974 int? _maxLines;
975
976 /// The value may be null. If it is not null, then it must be greater than zero.
977 set maxLines(int? value) {
978 assert(value == null || value > 0);
979 if (_maxLines == value) {
980 return;
981 }
982 _maxLines = value;
983 markNeedsLayout();
984 }
985
986 /// {@template flutter.painting.textPainter.strutStyle}
987 /// The strut style to use. Strut style defines the strut, which sets minimum
988 /// vertical layout metrics.
989 ///
990 /// Omitting or providing null will disable strut.
991 ///
992 /// Omitting or providing null for any properties of [StrutStyle] will result in
993 /// default values being used. It is highly recommended to at least specify a
994 /// [StrutStyle.fontSize].
995 ///
996 /// See [StrutStyle] for details.
997 /// {@endtemplate}
998 StrutStyle? get strutStyle => _strutStyle;
999 StrutStyle? _strutStyle;
1000 set strutStyle(StrutStyle? value) {
1001 if (_strutStyle == value) {
1002 return;
1003 }
1004 _strutStyle = value;
1005 markNeedsLayout();
1006 }
1007
1008 /// {@template flutter.painting.textPainter.textWidthBasis}
1009 /// Defines how to measure the width of the rendered text.
1010 /// {@endtemplate}
1011 TextWidthBasis get textWidthBasis => _textWidthBasis;
1012 TextWidthBasis _textWidthBasis;
1013 set textWidthBasis(TextWidthBasis value) {
1014 if (_textWidthBasis == value) {
1015 return;
1016 }
1017 assert(() {
1018 return _debugNeedsRelayout = true;
1019 }());
1020 _textWidthBasis = value;
1021 }
1022
1023 /// {@macro dart.ui.textHeightBehavior}
1024 TextHeightBehavior? get textHeightBehavior => _textHeightBehavior;
1025 TextHeightBehavior? _textHeightBehavior;
1026 set textHeightBehavior(TextHeightBehavior? value) {
1027 if (_textHeightBehavior == value) {
1028 return;
1029 }
1030 _textHeightBehavior = value;
1031 markNeedsLayout();
1032 }
1033
1034 /// An ordered list of [TextBox]es that bound the positions of the placeholders
1035 /// in the paragraph.
1036 ///
1037 /// Each box corresponds to a [PlaceholderSpan] in the order they were defined
1038 /// in the [InlineSpan] tree.
1039 List<TextBox>? get inlinePlaceholderBoxes {
1040 final _TextPainterLayoutCacheWithOffset? layout = _layoutCache;
1041 if (layout == null) {
1042 return null;
1043 }
1044 final Offset offset = layout.paintOffset;
1045 if (!offset.dx.isFinite || !offset.dy.isFinite) {
1046 return <TextBox>[];
1047 }
1048 final List<TextBox> rawBoxes = layout.inlinePlaceholderBoxes;
1049 if (offset == Offset.zero) {
1050 return rawBoxes;
1051 }
1052 return rawBoxes.map((TextBox box) => _shiftTextBox(box, offset)).toList(growable: false);
1053 }
1054
1055 /// Sets the dimensions of each placeholder in [text].
1056 ///
1057 /// The number of [PlaceholderDimensions] provided should be the same as the
1058 /// number of [PlaceholderSpan]s in text. Passing in an empty or null `value`
1059 /// will do nothing.
1060 ///
1061 /// If [layout] is attempted without setting the placeholder dimensions, the
1062 /// placeholders will be ignored in the text layout and no valid
1063 /// [inlinePlaceholderBoxes] will be returned.
1064 void setPlaceholderDimensions(List<PlaceholderDimensions>? value) {
1065 if (value == null || value.isEmpty || listEquals(value, _placeholderDimensions)) {
1066 return;
1067 }
1068 assert(() {
1069 int placeholderCount = 0;
1070 text!.visitChildren((InlineSpan span) {
1071 if (span is PlaceholderSpan) {
1072 placeholderCount += 1;
1073 }
1074 return value.length >= placeholderCount;
1075 });
1076 return placeholderCount == value.length;
1077 }());
1078 _placeholderDimensions = value;
1079 markNeedsLayout();
1080 }
1081
1082 List<PlaceholderDimensions>? _placeholderDimensions;
1083
1084 ui.ParagraphStyle _createParagraphStyle([TextAlign? textAlignOverride]) {
1085 assert(
1086 textDirection != null,
1087 'TextPainter.textDirection must be set to a non-null value before using the TextPainter.',
1088 );
1089 final TextStyle baseStyle = _text?.style ?? const TextStyle();
1090 return baseStyle.getParagraphStyle(
1091 textAlign: textAlignOverride ?? textAlign,
1092 textDirection: textDirection,
1093 textScaler: textScaler,
1094 maxLines: _maxLines,
1095 textHeightBehavior: _textHeightBehavior,
1096 ellipsis: _ellipsis,
1097 locale: _locale,
1098 strutStyle: _strutStyle,
1099 );
1100 }
1101
1102 ui.Paragraph? _layoutTemplate;
1103 ui.Paragraph _createLayoutTemplate() {
1104 final ui.ParagraphBuilder builder = ui.ParagraphBuilder(
1105 _createParagraphStyle(TextAlign.left),
1106 ); // direction doesn't matter, text is just a space
1107 final ui.TextStyle? textStyle = text?.style?.getTextStyle(textScaler: textScaler);
1108 if (textStyle != null) {
1109 builder.pushStyle(textStyle);
1110 }
1111 builder.addText(' ');
1112 return builder.build()..layout(const ui.ParagraphConstraints(width: double.infinity));
1113 }
1114
1115 ui.Paragraph _getOrCreateLayoutTemplate() => _layoutTemplate ??= _createLayoutTemplate();
1116
1117 /// The height of a space in [text] in logical pixels.
1118 ///
1119 /// Not every line of text in [text] will have this height, but this height
1120 /// is "typical" for text in [text] and useful for sizing other objects
1121 /// relative a typical line of text.
1122 ///
1123 /// Obtaining this value does not require calling [layout].
1124 ///
1125 /// The style of the [text] property is used to determine the font settings
1126 /// that contribute to the [preferredLineHeight]. If [text] is null or if it
1127 /// specifies no styles, the default [TextStyle] values are used (a 10 pixel
1128 /// sans-serif font).
1129 double get preferredLineHeight => _getOrCreateLayoutTemplate().height;
1130
1131 /// The width at which decreasing the width of the text would prevent it from
1132 /// painting itself completely within its bounds.
1133 ///
1134 /// Valid only after [layout] has been called.
1135 double get minIntrinsicWidth {
1136 assert(_debugAssertTextLayoutIsValid);
1137 return _layoutCache!.layout.minIntrinsicLineExtent;
1138 }
1139
1140 /// The width at which increasing the width of the text no longer decreases the height.
1141 ///
1142 /// Valid only after [layout] has been called.
1143 double get maxIntrinsicWidth {
1144 assert(_debugAssertTextLayoutIsValid);
1145 return _layoutCache!.layout.maxIntrinsicLineExtent;
1146 }
1147
1148 /// The horizontal space required to paint this text.
1149 ///
1150 /// Valid only after [layout] has been called.
1151 double get width {
1152 assert(_debugAssertTextLayoutIsValid);
1153 assert(!_debugNeedsRelayout);
1154 return _layoutCache!.contentWidth;
1155 }
1156
1157 /// The vertical space required to paint this text.
1158 ///
1159 /// Valid only after [layout] has been called.
1160 double get height {
1161 assert(_debugAssertTextLayoutIsValid);
1162 return _layoutCache!.layout.height;
1163 }
1164
1165 /// The amount of space required to paint this text.
1166 ///
1167 /// Valid only after [layout] has been called.
1168 Size get size {
1169 assert(_debugAssertTextLayoutIsValid);
1170 assert(!_debugNeedsRelayout);
1171 return Size(width, height);
1172 }
1173
1174 /// Returns the distance from the top of the text to the first baseline of the
1175 /// given type.
1176 ///
1177 /// Valid only after [layout] has been called.
1178 double computeDistanceToActualBaseline(TextBaseline baseline) {
1179 assert(_debugAssertTextLayoutIsValid);
1180 return _layoutCache!.layout.getDistanceToBaseline(baseline);
1181 }
1182
1183 /// Whether any text was truncated or ellipsized.
1184 ///
1185 /// If [maxLines] is not null, this is true if there were more lines to be
1186 /// drawn than the given [maxLines], and thus at least one line was omitted in
1187 /// the output; otherwise it is false.
1188 ///
1189 /// If [maxLines] is null, this is true if [ellipsis] is not the empty string
1190 /// and there was a line that overflowed the `maxWidth` argument passed to
1191 /// [layout]; otherwise it is false.
1192 ///
1193 /// Valid only after [layout] has been called.
1194 bool get didExceedMaxLines {
1195 assert(_debugAssertTextLayoutIsValid);
1196 return _layoutCache!.paragraph.didExceedMaxLines;
1197 }
1198
1199 // Creates a ui.Paragraph using the current configurations in this class and
1200 // assign it to _paragraph.
1201 ui.Paragraph _createParagraph(InlineSpan text) {
1202 final ui.ParagraphBuilder builder = ui.ParagraphBuilder(_createParagraphStyle());
1203 text.build(builder, textScaler: textScaler, dimensions: _placeholderDimensions);
1204 assert(() {
1205 _debugMarkNeedsLayoutCallStack = null;
1206 return true;
1207 }());
1208 _rebuildParagraphForPaint = false;
1209 return builder.build();
1210 }
1211
1212 /// Computes the visual position of the glyphs for painting the text.
1213 ///
1214 /// The text will layout with a width that's as close to its max intrinsic
1215 /// width (or its longest line, if [textWidthBasis] is set to
1216 /// [TextWidthBasis.parent]) as possible while still being greater than or
1217 /// equal to `minWidth` and less than or equal to `maxWidth`.
1218 ///
1219 /// The [text] and [textDirection] properties must be non-null before this is
1220 /// called.
1221 void layout({double minWidth = 0.0, double maxWidth = double.infinity}) {
1222 assert(!maxWidth.isNaN);
1223 assert(!minWidth.isNaN);
1224 assert(() {
1225 _debugNeedsRelayout = false;
1226 return true;
1227 }());
1228
1229 final _TextPainterLayoutCacheWithOffset? cachedLayout = _layoutCache;
1230 if (cachedLayout != null && cachedLayout._resizeToFit(minWidth, maxWidth, textWidthBasis)) {
1231 return;
1232 }
1233
1234 final InlineSpan? text = this.text;
1235 if (text == null) {
1236 throw StateError(
1237 'TextPainter.text must be set to a non-null value before using the TextPainter.',
1238 );
1239 }
1240 final TextDirection? textDirection = this.textDirection;
1241 if (textDirection == null) {
1242 throw StateError(
1243 'TextPainter.textDirection must be set to a non-null value before using the TextPainter.',
1244 );
1245 }
1246
1247 final double paintOffsetAlignment = _computePaintOffsetFraction(textAlign, textDirection);
1248 // Try to avoid laying out the paragraph with maxWidth=double.infinity
1249 // when the text is not left-aligned, so we don't have to deal with an
1250 // infinite paint offset.
1251 final bool adjustMaxWidth = !maxWidth.isFinite && paintOffsetAlignment != 0;
1252 final double? adjustedMaxWidth = !adjustMaxWidth
1253 ? maxWidth
1254 : cachedLayout?.layout.maxIntrinsicLineExtent;
1255 final double layoutMaxWidth = adjustedMaxWidth ?? maxWidth;
1256
1257 // Only rebuild the paragraph when there're layout changes, even when
1258 // `_rebuildParagraphForPaint` is true. It's best to not eagerly rebuild
1259 // the paragraph to avoid the extra work, because:
1260 // 1. the text color could change again before `paint` is called (so one of
1261 // the paragraph rebuilds is unnecessary)
1262 // 2. the user could be measuring the text layout so `paint` will never be
1263 // called.
1264 final ui.Paragraph paragraph = (cachedLayout?.paragraph ?? _createParagraph(text))
1265 ..layout(ui.ParagraphConstraints(width: layoutMaxWidth));
1266 final _TextLayout layout = _TextLayout._(paragraph, textDirection, this);
1267 final double contentWidth = layout._contentWidthFor(minWidth, maxWidth, textWidthBasis);
1268
1269 final _TextPainterLayoutCacheWithOffset newLayoutCache;
1270 // Call layout again if newLayoutCache had an infinite paint offset.
1271 // This is not as expensive as it seems, line breaking is relatively cheap
1272 // as compared to shaping.
1273 if (adjustedMaxWidth == null && minWidth.isFinite) {
1274 assert(maxWidth.isInfinite);
1275 final double newInputWidth = layout.maxIntrinsicLineExtent;
1276 paragraph.layout(ui.ParagraphConstraints(width: newInputWidth));
1277 newLayoutCache = _TextPainterLayoutCacheWithOffset(
1278 layout,
1279 paintOffsetAlignment,
1280 newInputWidth,
1281 contentWidth,
1282 );
1283 } else {
1284 newLayoutCache = _TextPainterLayoutCacheWithOffset(
1285 layout,
1286 paintOffsetAlignment,
1287 layoutMaxWidth,
1288 contentWidth,
1289 );
1290 }
1291 _layoutCache = newLayoutCache;
1292 }
1293
1294 /// Paints the text onto the given canvas at the given offset.
1295 ///
1296 /// Valid only after [layout] has been called.
1297 ///
1298 /// If you cannot see the text being painted, check that your text color does
1299 /// not conflict with the background on which you are drawing. The default
1300 /// text color is white (to contrast with the default black background color),
1301 /// so if you are writing an application with a white background, the text
1302 /// will not be visible by default.
1303 ///
1304 /// To set the text style, specify a [TextStyle] when creating the [TextSpan]
1305 /// that you pass to the [TextPainter] constructor or to the [text] property.
1306 void paint(Canvas canvas, Offset offset) {
1307 final _TextPainterLayoutCacheWithOffset? layoutCache = _layoutCache;
1308 if (layoutCache == null) {
1309 throw StateError(
1310 'TextPainter.paint called when text geometry was not yet calculated.\n'
1311 'Please call layout() before paint() to position the text before painting it.',
1312 );
1313 }
1314
1315 if (!layoutCache.paintOffset.dx.isFinite || !layoutCache.paintOffset.dy.isFinite) {
1316 return;
1317 }
1318
1319 if (_rebuildParagraphForPaint) {
1320 Size? debugSize;
1321 assert(() {
1322 debugSize = size;
1323 return true;
1324 }());
1325
1326 final ui.Paragraph paragraph = layoutCache.paragraph;
1327 // Unfortunately even if we know that there is only paint changes, there's
1328 // no API to only make those updates so the paragraph has to be recreated
1329 // and re-laid out.
1330 assert(!layoutCache.layoutMaxWidth.isNaN);
1331 layoutCache.layout._paragraph = _createParagraph(text!)
1332 ..layout(ui.ParagraphConstraints(width: layoutCache.layoutMaxWidth));
1333 assert(paragraph.width == layoutCache.layout._paragraph.width);
1334 paragraph.dispose();
1335 assert(debugSize == size);
1336 }
1337 assert(!_rebuildParagraphForPaint);
1338 canvas.drawParagraph(layoutCache.paragraph, offset + layoutCache.paintOffset);
1339 }
1340
1341 // Returns true if value falls in the valid range of the UTF16 encoding.
1342 static bool _isUTF16(int value) {
1343 return value >= 0x0 && value <= 0xFFFFF;
1344 }
1345
1346 /// Returns true iff the given value is a valid UTF-16 high (first) surrogate.
1347 /// The value must be a UTF-16 code unit, meaning it must be in the range
1348 /// 0x0000-0xFFFF.
1349 ///
1350 /// See also:
1351 /// * https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF
1352 /// * [isLowSurrogate], which checks the same thing for low (second)
1353 /// surrogates.
1354 static bool isHighSurrogate(int value) {
1355 assert(_isUTF16(value));
1356 return value & 0xFC00 == 0xD800;
1357 }
1358
1359 /// Returns true iff the given value is a valid UTF-16 low (second) surrogate.
1360 /// The value must be a UTF-16 code unit, meaning it must be in the range
1361 /// 0x0000-0xFFFF.
1362 ///
1363 /// See also:
1364 /// * https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF
1365 /// * [isHighSurrogate], which checks the same thing for high (first)
1366 /// surrogates.
1367 static bool isLowSurrogate(int value) {
1368 assert(_isUTF16(value));
1369 return value & 0xFC00 == 0xDC00;
1370 }
1371
1372 /// Returns the closest offset after `offset` at which the input cursor can be
1373 /// positioned.
1374 int? getOffsetAfter(int offset) {
1375 final int? nextCodeUnit = _text!.codeUnitAt(offset);
1376 if (nextCodeUnit == null) {
1377 return null;
1378 }
1379 // TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404).
1380 return isHighSurrogate(nextCodeUnit) ? offset + 2 : offset + 1;
1381 }
1382
1383 /// Returns the closest offset before `offset` at which the input cursor can
1384 /// be positioned.
1385 int? getOffsetBefore(int offset) {
1386 final int? prevCodeUnit = _text!.codeUnitAt(offset - 1);
1387 if (prevCodeUnit == null) {
1388 return null;
1389 }
1390 // TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404).
1391 return isLowSurrogate(prevCodeUnit) ? offset - 2 : offset - 1;
1392 }
1393
1394 static double _computePaintOffsetFraction(TextAlign textAlign, TextDirection textDirection) {
1395 return switch ((textAlign, textDirection)) {
1396 (TextAlign.left, _) => 0.0,
1397 (TextAlign.right, _) => 1.0,
1398 (TextAlign.center, _) => 0.5,
1399 (TextAlign.start || TextAlign.justify, TextDirection.ltr) => 0.0,
1400 (TextAlign.start || TextAlign.justify, TextDirection.rtl) => 1.0,
1401 (TextAlign.end, TextDirection.ltr) => 1.0,
1402 (TextAlign.end, TextDirection.rtl) => 0.0,
1403 };
1404 }
1405
1406 /// Returns the offset at which to paint the caret.
1407 ///
1408 /// Valid only after [layout] has been called.
1409 Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
1410 final _TextPainterLayoutCacheWithOffset layoutCache = _layoutCache!;
1411 final _LineCaretMetrics? caretMetrics = _computeCaretMetrics(position);
1412
1413 if (caretMetrics == null) {
1414 final double paintOffsetAlignment = _computePaintOffsetFraction(textAlign, textDirection!);
1415 // The full width is not (width - caretPrototype.width), because
1416 // RenderEditable reserves cursor width on the right. Ideally this
1417 // should be handled by RenderEditable instead.
1418 final double dx = paintOffsetAlignment == 0
1419 ? 0
1420 : paintOffsetAlignment * layoutCache.contentWidth;
1421 return Offset(dx, 0.0);
1422 }
1423
1424 final Offset rawOffset = switch (caretMetrics) {
1425 _LineCaretMetrics(writingDirection: TextDirection.ltr, :final Offset offset) => offset,
1426 _LineCaretMetrics(writingDirection: TextDirection.rtl, :final Offset offset) => Offset(
1427 offset.dx - caretPrototype.width,
1428 offset.dy,
1429 ),
1430 };
1431 // If offset.dx is outside of the advertised content area, then the associated
1432 // glyph belongs to a trailing whitespace character. Ideally the behavior
1433 // should be handled by higher-level implementations (for instance,
1434 // RenderEditable reserves width for showing the caret, it's best to handle
1435 // the clamping there).
1436 final double adjustedDx = clampDouble(
1437 rawOffset.dx + layoutCache.paintOffset.dx,
1438 0,
1439 layoutCache.contentWidth,
1440 );
1441 return Offset(adjustedDx, rawOffset.dy + layoutCache.paintOffset.dy);
1442 }
1443
1444 /// {@template flutter.painting.textPainter.getFullHeightForCaret}
1445 /// Returns the strut bounded height of the glyph at the given `position`.
1446 /// {@endtemplate}
1447 ///
1448 /// Valid only after [layout] has been called.
1449 double getFullHeightForCaret(TextPosition position, Rect caretPrototype) {
1450 // The if condition is derived from
1451 // https://github.com/google/skia/blob/0086a17e0d4cc676cf88cae671ba5ee967eb7241/modules/skparagraph/src/TextLine.cpp#L1244-L1246
1452 // which is set here:
1453 // https://github.com/flutter/engine/blob/a821b8790c9fd0e095013cd5bd1f20273bc1ee47/third_party/txt/src/skia/paragraph_builder_skia.cc#L134
1454 if (strutStyle == null || strutStyle == StrutStyle.disabled || strutStyle?.fontSize == 0.0) {
1455 final double? heightFromCaretMetrics = _computeCaretMetrics(position)?.height;
1456 if (heightFromCaretMetrics != null) {
1457 return heightFromCaretMetrics;
1458 }
1459 }
1460 final TextBox textBox = _getOrCreateLayoutTemplate()
1461 .getBoxesForRange(0, 1, boxHeightStyle: ui.BoxHeightStyle.strut)
1462 .single;
1463 return textBox.toRect().height;
1464 }
1465
1466 bool _isNewlineAtOffset(int offset) =>
1467 0 <= offset &&
1468 offset < plainText.length &&
1469 WordBoundary._isNewline(plainText.codeUnitAt(offset));
1470
1471 // Cached caret metrics. This allows multiple invokes of [getOffsetForCaret] and
1472 // [getFullHeightForCaret] in a row without performing redundant and expensive
1473 // get rect calls to the paragraph.
1474 //
1475 // The cache implementation assumes there's only one cursor at any given time.
1476 late _LineCaretMetrics _caretMetrics;
1477
1478 // This function returns the caret's offset and height for the given
1479 // `position` in the text, or null if the paragraph is empty.
1480 //
1481 // For a TextPosition, typically when its TextAffinity is downstream, the
1482 // corresponding I-beam caret is anchored to the leading edge of the character
1483 // at `offset` in the text. When the TextAffinity is upstream, the I-beam is
1484 // then anchored to the trailing edge of the preceding character, except for a
1485 // few edge cases:
1486 //
1487 // 1. empty paragraph: this method returns null and the caller handles this
1488 // case.
1489 //
1490 // 2. (textLength, downstream), the end-of-text caret when the text is not
1491 // empty: it's placed next to the trailing edge of the last line of the
1492 // text, in case the text and its last bidi run have different writing
1493 // directions. See the `_computeEndOfTextCaretAnchorOffset` method for more
1494 // details.
1495 //
1496 // 3. (0, upstream), which isn't a valid position, but it's not a conventional
1497 // "invalid" caret location either (the offset isn't negative). For
1498 // historical reasons, this is treated as (0, downstream).
1499 //
1500 // 4. (x, upstream) where x - 1 points to a line break character. The caret
1501 // should be displayed at the beginning of the newline instead of at the
1502 // end of the previous line. Converts the location to (x, downstream). The
1503 // choice we makes in 5. allows us to still check (x - 1) in case x points
1504 // to a multi-code-unit character.
1505 //
1506 // 5. (x, downstream || upstream), where x points to a multi-code-unit
1507 // character. There's no perfect caret placement in this case. Here we chose
1508 // to draw the caret at the location that makes the most sense when the
1509 // user wants to backspace (which also means it's left-arrow-key-biased):
1510 //
1511 // * downstream: show the caret at the leading edge of the character only if
1512 // x points to the start of the grapheme. Otherwise show the caret at the
1513 // leading edge of the next logical character.
1514 // * upstream: show the caret at the trailing edge of the previous character
1515 // only if x points to the start of the grapheme. Otherwise place the
1516 // caret at the trailing edge of the character.
1517 _LineCaretMetrics? _computeCaretMetrics(TextPosition position) {
1518 assert(_debugAssertTextLayoutIsValid);
1519 assert(!_debugNeedsRelayout);
1520
1521 final _TextPainterLayoutCacheWithOffset cachedLayout = _layoutCache!;
1522 // If nothing is laid out, top start is the only reasonable place to place
1523 // the cursor.
1524 if (cachedLayout.paragraph.numberOfLines < 1) {
1525 // TODO(LongCatIsLooong): assert when an invalid position is given.
1526 return null;
1527 }
1528
1529 final (int offset, bool anchorToLeadingEdge) = switch (position) {
1530 TextPosition(offset: 0) => (
1531 0,
1532 true,
1533 ), // As a special case, always anchor to the leading edge of the first grapheme regardless of the affinity.
1534 TextPosition(:final int offset, affinity: TextAffinity.downstream) => (offset, true),
1535 TextPosition(:final int offset, affinity: TextAffinity.upstream)
1536 when _isNewlineAtOffset(offset - 1) =>
1537 (offset, true),
1538 TextPosition(:final int offset, affinity: TextAffinity.upstream) => (offset - 1, false),
1539 };
1540
1541 final int caretPositionCacheKey = anchorToLeadingEdge ? offset : -offset - 1;
1542 if (caretPositionCacheKey == cachedLayout._previousCaretPositionKey) {
1543 return _caretMetrics;
1544 }
1545
1546 final ui.GlyphInfo? glyphInfo = cachedLayout.paragraph.getGlyphInfoAt(offset);
1547
1548 if (glyphInfo == null) {
1549 // If the glyph isn't laid out, then the position points to a character
1550 // that is not laid out (the part of text is invisible due to maxLines or
1551 // infinite paragraph x offset). Use the EOT caret.
1552 // TODO(LongCatIsLooong): assert when an invalid position is given.
1553 final ui.Paragraph template = _getOrCreateLayoutTemplate();
1554 assert(template.numberOfLines == 1);
1555 final double baselineOffset = template.getLineMetricsAt(0)!.baseline;
1556 return cachedLayout.layout._endOfTextCaretMetrics.shift(Offset(0.0, -baselineOffset));
1557 }
1558
1559 final TextRange graphemeRange = glyphInfo.graphemeClusterCodeUnitRange;
1560
1561 // Works around a SkParagraph bug (https://github.com/flutter/flutter/issues/120836#issuecomment-1937343854):
1562 // placeholders with a size of (0, 0) always have a rect of Rect.zero and a
1563 // range of (0, 0).
1564 if (graphemeRange.isCollapsed) {
1565 assert(graphemeRange.start == 0);
1566 return _computeCaretMetrics(TextPosition(offset: offset + 1));
1567 }
1568 if (anchorToLeadingEdge && graphemeRange.start != offset) {
1569 assert(graphemeRange.end > graphemeRange.start + 1);
1570 // Addresses the case where `offset` points to a multi-code-unit grapheme
1571 // that doesn't start at `offset`.
1572 return _computeCaretMetrics(TextPosition(offset: graphemeRange.end));
1573 }
1574
1575 final _LineCaretMetrics metrics;
1576 final List<TextBox> boxes = cachedLayout.paragraph.getBoxesForRange(
1577 graphemeRange.start,
1578 graphemeRange.end,
1579 boxHeightStyle: ui.BoxHeightStyle.strut,
1580 );
1581
1582 final bool anchorToLeft = switch (glyphInfo.writingDirection) {
1583 TextDirection.ltr => anchorToLeadingEdge,
1584 TextDirection.rtl => !anchorToLeadingEdge,
1585 };
1586 final TextBox box = anchorToLeft ? boxes.first : boxes.last;
1587 metrics = _LineCaretMetrics(
1588 offset: Offset(anchorToLeft ? box.left : box.right, box.top),
1589 writingDirection: box.direction,
1590 height: box.bottom - box.top,
1591 );
1592
1593 cachedLayout._previousCaretPositionKey = caretPositionCacheKey;
1594 return _caretMetrics = metrics;
1595 }
1596
1597 /// Returns a list of rects that bound the given selection.
1598 ///
1599 /// The [selection] must be a valid range (with [TextSelection.isValid] true).
1600 ///
1601 /// The [boxHeightStyle] and [boxWidthStyle] arguments may be used to select
1602 /// the shape of the [TextBox]s. These properties default to
1603 /// [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively.
1604 ///
1605 /// A given selection might have more than one rect if this text painter
1606 /// contains bidirectional text because logically contiguous text might not be
1607 /// visually contiguous.
1608 ///
1609 /// Leading or trailing newline characters will be represented by zero-width
1610 /// `TextBox`es.
1611 ///
1612 /// The method only returns `TextBox`es of glyphs that are entirely enclosed by
1613 /// the given `selection`: a multi-code-unit glyph will be excluded if only
1614 /// part of its code units are in `selection`.
1615 List<TextBox> getBoxesForSelection(
1616 TextSelection selection, {
1617 ui.BoxHeightStyle boxHeightStyle = ui.BoxHeightStyle.tight,
1618 ui.BoxWidthStyle boxWidthStyle = ui.BoxWidthStyle.tight,
1619 }) {
1620 assert(_debugAssertTextLayoutIsValid);
1621 assert(selection.isValid);
1622 assert(!_debugNeedsRelayout);
1623 final _TextPainterLayoutCacheWithOffset cachedLayout = _layoutCache!;
1624 final Offset offset = cachedLayout.paintOffset;
1625 if (!offset.dx.isFinite || !offset.dy.isFinite) {
1626 return <TextBox>[];
1627 }
1628 final List<TextBox> boxes = cachedLayout.paragraph.getBoxesForRange(
1629 selection.start,
1630 selection.end,
1631 boxHeightStyle: boxHeightStyle,
1632 boxWidthStyle: boxWidthStyle,
1633 );
1634 return offset == Offset.zero
1635 ? boxes
1636 : boxes.map((TextBox box) => _shiftTextBox(box, offset)).toList(growable: false);
1637 }
1638
1639 /// Returns the [GlyphInfo] of the glyph closest to the given `offset` in the
1640 /// paragraph coordinate system, or null if the text is empty, or is entirely
1641 /// clipped or ellipsized away.
1642 ///
1643 /// This method first finds the line closest to `offset.dy`, and then returns
1644 /// the [GlyphInfo] of the closest glyph(s) within that line.
1645 ui.GlyphInfo? getClosestGlyphForOffset(Offset offset) {
1646 assert(_debugAssertTextLayoutIsValid);
1647 assert(!_debugNeedsRelayout);
1648 final _TextPainterLayoutCacheWithOffset cachedLayout = _layoutCache!;
1649 final ui.GlyphInfo? rawGlyphInfo = cachedLayout.paragraph.getClosestGlyphInfoForOffset(
1650 offset - cachedLayout.paintOffset,
1651 );
1652 if (rawGlyphInfo == null || cachedLayout.paintOffset == Offset.zero) {
1653 return rawGlyphInfo;
1654 }
1655 return ui.GlyphInfo(
1656 rawGlyphInfo.graphemeClusterLayoutBounds.shift(cachedLayout.paintOffset),
1657 rawGlyphInfo.graphemeClusterCodeUnitRange,
1658 rawGlyphInfo.writingDirection,
1659 );
1660 }
1661
1662 /// Returns the closest position within the text for the given pixel offset.
1663 TextPosition getPositionForOffset(Offset offset) {
1664 assert(_debugAssertTextLayoutIsValid);
1665 assert(!_debugNeedsRelayout);
1666 final _TextPainterLayoutCacheWithOffset cachedLayout = _layoutCache!;
1667 return cachedLayout.paragraph.getPositionForOffset(offset - cachedLayout.paintOffset);
1668 }
1669
1670 /// {@template flutter.painting.TextPainter.getWordBoundary}
1671 /// Returns the text range of the word at the given offset. Characters not
1672 /// part of a word, such as spaces, symbols, and punctuation, have word breaks
1673 /// on both sides. In such cases, this method will return a text range that
1674 /// contains the given text position.
1675 ///
1676 /// Word boundaries are defined more precisely in Unicode Standard Annex #29
1677 /// <http://www.unicode.org/reports/tr29/#Word_Boundaries>.
1678 /// {@endtemplate}
1679 TextRange getWordBoundary(TextPosition position) {
1680 assert(_debugAssertTextLayoutIsValid);
1681 return _layoutCache!.paragraph.getWordBoundary(position);
1682 }
1683
1684 /// {@template flutter.painting.TextPainter.wordBoundaries}
1685 /// Returns a [TextBoundary] that can be used to perform word boundary analysis
1686 /// on the current [text].
1687 ///
1688 /// This [TextBoundary] uses word boundary rules defined in [Unicode Standard
1689 /// Annex #29](http://www.unicode.org/reports/tr29/#Word_Boundaries).
1690 /// {@endtemplate}
1691 ///
1692 /// Currently word boundary analysis can only be performed after [layout]
1693 /// has been called.
1694 WordBoundary get wordBoundaries => WordBoundary._(text!, _layoutCache!.paragraph);
1695
1696 /// Returns the text range of the line at the given offset.
1697 ///
1698 /// The newline (if any) is not returned as part of the range.
1699 TextRange getLineBoundary(TextPosition position) {
1700 assert(_debugAssertTextLayoutIsValid);
1701 return _layoutCache!.paragraph.getLineBoundary(position);
1702 }
1703
1704 static ui.LineMetrics _shiftLineMetrics(ui.LineMetrics metrics, Offset offset) {
1705 assert(offset.dx.isFinite);
1706 assert(offset.dy.isFinite);
1707 return ui.LineMetrics(
1708 hardBreak: metrics.hardBreak,
1709 ascent: metrics.ascent,
1710 descent: metrics.descent,
1711 unscaledAscent: metrics.unscaledAscent,
1712 height: metrics.height,
1713 width: metrics.width,
1714 left: metrics.left + offset.dx,
1715 baseline: metrics.baseline + offset.dy,
1716 lineNumber: metrics.lineNumber,
1717 );
1718 }
1719
1720 static TextBox _shiftTextBox(TextBox box, Offset offset) {
1721 assert(offset.dx.isFinite);
1722 assert(offset.dy.isFinite);
1723 return TextBox.fromLTRBD(
1724 box.left + offset.dx,
1725 box.top + offset.dy,
1726 box.right + offset.dx,
1727 box.bottom + offset.dy,
1728 box.direction,
1729 );
1730 }
1731
1732 /// Returns the full list of [LineMetrics] that describe in detail the various
1733 /// metrics of each laid out line.
1734 ///
1735 /// The [LineMetrics] list is presented in the order of the lines they represent.
1736 /// For example, the first line is in the zeroth index.
1737 ///
1738 /// [LineMetrics] contains measurements such as ascent, descent, baseline, and
1739 /// width for the line as a whole, and may be useful for aligning additional
1740 /// widgets to a particular line.
1741 ///
1742 /// Valid only after [layout] has been called.
1743 List<ui.LineMetrics> computeLineMetrics() {
1744 assert(_debugAssertTextLayoutIsValid);
1745 assert(!_debugNeedsRelayout);
1746 final _TextPainterLayoutCacheWithOffset layout = _layoutCache!;
1747 final Offset offset = layout.paintOffset;
1748 if (!offset.dx.isFinite || !offset.dy.isFinite) {
1749 return const <ui.LineMetrics>[];
1750 }
1751 final List<ui.LineMetrics> rawMetrics = layout.lineMetrics;
1752 return offset == Offset.zero
1753 ? rawMetrics
1754 : rawMetrics
1755 .map((ui.LineMetrics metrics) => _shiftLineMetrics(metrics, offset))
1756 .toList(growable: false);
1757 }
1758
1759 bool _disposed = false;
1760
1761 /// Whether this object has been disposed or not.
1762 ///
1763 /// Only for use when asserts are enabled.
1764 bool get debugDisposed {
1765 bool? disposed;
1766 assert(() {
1767 disposed = _disposed;
1768 return true;
1769 }());
1770 return disposed ?? (throw StateError('debugDisposed only available when asserts are on.'));
1771 }
1772
1773 /// Releases the resources associated with this painter.
1774 ///
1775 /// After disposal this painter is unusable.
1776 void dispose() {
1777 assert(!debugDisposed);
1778 assert(() {
1779 _disposed = true;
1780 return true;
1781 }());
1782 assert(debugMaybeDispatchDisposed(this));
1783 _layoutTemplate?.dispose();
1784 _layoutTemplate = null;
1785 _layoutCache?.paragraph.dispose();
1786 _layoutCache = null;
1787 _text = null;
1788 }
1789}
1790
1791class _UnspecifiedTextScaler extends TextScaler {
1792 const _UnspecifiedTextScaler();
1793 @override
1794 Never get textScaleFactor => throw UnimplementedError();
1795
1796 @override
1797 Never scale(double fontSize) => throw UnimplementedError();
1798}
1799