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 = TextScaler.noScaling,
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, TextScaler.noScaling),
617 'Use textScaler instead.',
618 ),
619 _text = text,
620 _textAlign = textAlign,
621 _textDirection = textDirection,
622 _textScaler =
623 textScaler == TextScaler.noScaling ? TextScaler.linear(textScaleFactor) : textScaler,
624 _maxLines = maxLines,
625 _ellipsis = ellipsis,
626 _locale = locale,
627 _strutStyle = strutStyle,
628 _textWidthBasis = textWidthBasis,
629 _textHeightBehavior = textHeightBehavior {
630 assert(debugMaybeDispatchCreated('painting', 'TextPainter', this));
631 }
632
633 /// Computes the width of a configured [TextPainter].
634 ///
635 /// This is a convenience method that creates a text painter with the supplied
636 /// parameters, lays it out with the supplied [minWidth] and [maxWidth], and
637 /// returns its [TextPainter.width] making sure to dispose the underlying
638 /// resources. Doing this operation is expensive and should be avoided
639 /// whenever it is possible to preserve the [TextPainter] to paint the
640 /// text or get other information about it.
641 static double computeWidth({
642 required InlineSpan text,
643 required TextDirection textDirection,
644 TextAlign textAlign = TextAlign.start,
645 @Deprecated(
646 'Use textScaler instead. '
647 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
648 'This feature was deprecated after v3.12.0-2.0.pre.',
649 )
650 double textScaleFactor = 1.0,
651 TextScaler textScaler = TextScaler.noScaling,
652 int? maxLines,
653 String? ellipsis,
654 Locale? locale,
655 StrutStyle? strutStyle,
656 TextWidthBasis textWidthBasis = TextWidthBasis.parent,
657 TextHeightBehavior? textHeightBehavior,
658 double minWidth = 0.0,
659 double maxWidth = double.infinity,
660 }) {
661 assert(
662 textScaleFactor == 1.0 || identical(textScaler, TextScaler.noScaling),
663 'Use textScaler instead.',
664 );
665 final TextPainter painter = TextPainter(
666 text: text,
667 textAlign: textAlign,
668 textDirection: textDirection,
669 textScaler:
670 textScaler == TextScaler.noScaling ? TextScaler.linear(textScaleFactor) : textScaler,
671 maxLines: maxLines,
672 ellipsis: ellipsis,
673 locale: locale,
674 strutStyle: strutStyle,
675 textWidthBasis: textWidthBasis,
676 textHeightBehavior: textHeightBehavior,
677 )..layout(minWidth: minWidth, maxWidth: maxWidth);
678
679 try {
680 return painter.width;
681 } finally {
682 painter.dispose();
683 }
684 }
685
686 /// Computes the max intrinsic width of a configured [TextPainter].
687 ///
688 /// This is a convenience method that creates a text painter with the supplied
689 /// parameters, lays it out with the supplied [minWidth] and [maxWidth], and
690 /// returns its [TextPainter.maxIntrinsicWidth] making sure to dispose the
691 /// underlying resources. Doing this operation is expensive and should be avoided
692 /// whenever it is possible to preserve the [TextPainter] to paint the
693 /// text or get other information about it.
694 static double computeMaxIntrinsicWidth({
695 required InlineSpan text,
696 required TextDirection textDirection,
697 TextAlign textAlign = TextAlign.start,
698 @Deprecated(
699 'Use textScaler instead. '
700 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
701 'This feature was deprecated after v3.12.0-2.0.pre.',
702 )
703 double textScaleFactor = 1.0,
704 TextScaler textScaler = TextScaler.noScaling,
705 int? maxLines,
706 String? ellipsis,
707 Locale? locale,
708 StrutStyle? strutStyle,
709 TextWidthBasis textWidthBasis = TextWidthBasis.parent,
710 TextHeightBehavior? textHeightBehavior,
711 double minWidth = 0.0,
712 double maxWidth = double.infinity,
713 }) {
714 assert(
715 textScaleFactor == 1.0 || identical(textScaler, TextScaler.noScaling),
716 'Use textScaler instead.',
717 );
718 final TextPainter painter = TextPainter(
719 text: text,
720 textAlign: textAlign,
721 textDirection: textDirection,
722 textScaler:
723 textScaler == TextScaler.noScaling ? TextScaler.linear(textScaleFactor) : textScaler,
724 maxLines: maxLines,
725 ellipsis: ellipsis,
726 locale: locale,
727 strutStyle: strutStyle,
728 textWidthBasis: textWidthBasis,
729 textHeightBehavior: textHeightBehavior,
730 )..layout(minWidth: minWidth, maxWidth: maxWidth);
731
732 try {
733 return painter.maxIntrinsicWidth;
734 } finally {
735 painter.dispose();
736 }
737 }
738
739 // Whether textWidthBasis has changed after the most recent `layout` call.
740 bool _debugNeedsRelayout = true;
741 // The result of the most recent `layout` call.
742 _TextPainterLayoutCacheWithOffset? _layoutCache;
743
744 // Whether _layoutCache contains outdated paint information and needs to be
745 // updated before painting.
746 //
747 // ui.Paragraph is entirely immutable, thus text style changes that can affect
748 // layout and those who can't both require the ui.Paragraph object being
749 // recreated. The caller may not call `layout` again after text color is
750 // updated. See: https://github.com/flutter/flutter/issues/85108
751 bool _rebuildParagraphForPaint = true;
752
753 bool get _debugAssertTextLayoutIsValid {
754 assert(!debugDisposed);
755 if (_layoutCache == null) {
756 throw FlutterError.fromParts(<DiagnosticsNode>[
757 ErrorSummary('Text layout not available'),
758 if (_debugMarkNeedsLayoutCallStack != null)
759 DiagnosticsStackTrace(
760 'The calls that first invalidated the text layout were',
761 _debugMarkNeedsLayoutCallStack,
762 )
763 else
764 ErrorDescription('The TextPainter has never been laid out.'),
765 ]);
766 }
767 return true;
768 }
769
770 StackTrace? _debugMarkNeedsLayoutCallStack;
771
772 /// Marks this text painter's layout information as dirty and removes cached
773 /// information.
774 ///
775 /// Uses this method to notify text painter to relayout in the case of
776 /// layout changes in engine. In most cases, updating text painter properties
777 /// in framework will automatically invoke this method.
778 void markNeedsLayout() {
779 assert(() {
780 if (_layoutCache != null) {
781 _debugMarkNeedsLayoutCallStack ??= StackTrace.current;
782 }
783 return true;
784 }());
785 _layoutCache?.paragraph.dispose();
786 _layoutCache = null;
787 }
788
789 /// The (potentially styled) text to paint.
790 ///
791 /// After this is set, you must call [layout] before the next call to [paint].
792 /// This and [textDirection] must be non-null before you call [layout].
793 ///
794 /// The [InlineSpan] this provides is in the form of a tree that may contain
795 /// multiple instances of [TextSpan]s and [WidgetSpan]s. To obtain a plain text
796 /// representation of the contents of this [TextPainter], use [plainText].
797 InlineSpan? get text => _text;
798 InlineSpan? _text;
799 set text(InlineSpan? value) {
800 assert(value == null || value.debugAssertIsValid());
801 if (_text == value) {
802 return;
803 }
804 if (_text?.style != value?.style) {
805 _layoutTemplate?.dispose();
806 _layoutTemplate = null;
807 }
808
809 final RenderComparison comparison =
810 value == null
811 ? RenderComparison.layout
812 : _text?.compareTo(value) ?? RenderComparison.layout;
813
814 _text = value;
815 _cachedPlainText = null;
816
817 if (comparison.index >= RenderComparison.layout.index) {
818 markNeedsLayout();
819 } else if (comparison.index >= RenderComparison.paint.index) {
820 // Don't invalid the _layoutCache just yet. It still contains valid layout
821 // information.
822 _rebuildParagraphForPaint = true;
823 }
824 // Neither relayout or repaint is needed.
825 }
826
827 /// Returns a plain text version of the text to paint.
828 ///
829 /// This uses [InlineSpan.toPlainText] to get the full contents of all nodes in the tree.
830 String get plainText {
831 _cachedPlainText ??= _text?.toPlainText(includeSemanticsLabels: false);
832 return _cachedPlainText ?? '';
833 }
834
835 String? _cachedPlainText;
836
837 /// How the text should be aligned horizontally.
838 ///
839 /// After this is set, you must call [layout] before the next call to [paint].
840 ///
841 /// The [textAlign] property defaults to [TextAlign.start].
842 TextAlign get textAlign => _textAlign;
843 TextAlign _textAlign;
844 set textAlign(TextAlign value) {
845 if (_textAlign == value) {
846 return;
847 }
848 _textAlign = value;
849 markNeedsLayout();
850 }
851
852 /// The default directionality of the text.
853 ///
854 /// This controls how the [TextAlign.start], [TextAlign.end], and
855 /// [TextAlign.justify] values of [textAlign] are resolved.
856 ///
857 /// This is also used to disambiguate how to render bidirectional text. For
858 /// example, if the [text] is an English phrase followed by a Hebrew phrase,
859 /// in a [TextDirection.ltr] context the English phrase will be on the left
860 /// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
861 /// context, the English phrase will be on the right and the Hebrew phrase on
862 /// its left.
863 ///
864 /// After this is set, you must call [layout] before the next call to [paint].
865 ///
866 /// This and [text] must be non-null before you call [layout].
867 TextDirection? get textDirection => _textDirection;
868 TextDirection? _textDirection;
869 set textDirection(TextDirection? value) {
870 if (_textDirection == value) {
871 return;
872 }
873 _textDirection = value;
874 markNeedsLayout();
875 _layoutTemplate?.dispose();
876 _layoutTemplate = null; // Shouldn't really matter, but for strict correctness...
877 }
878
879 /// Deprecated. Will be removed in a future version of Flutter. Use
880 /// [textScaler] instead.
881 ///
882 /// The number of font pixels for each logical pixel.
883 ///
884 /// For example, if the text scale factor is 1.5, text will be 50% larger than
885 /// the specified font size.
886 ///
887 /// After this is set, you must call [layout] before the next call to [paint].
888 @Deprecated(
889 'Use textScaler instead. '
890 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
891 'This feature was deprecated after v3.12.0-2.0.pre.',
892 )
893 double get textScaleFactor => textScaler.textScaleFactor;
894 @Deprecated(
895 'Use textScaler instead. '
896 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
897 'This feature was deprecated after v3.12.0-2.0.pre.',
898 )
899 set textScaleFactor(double value) {
900 textScaler = TextScaler.linear(value);
901 }
902
903 /// {@template flutter.painting.textPainter.textScaler}
904 /// The font scaling strategy to use when laying out and rendering the text.
905 ///
906 /// The value usually comes from [MediaQuery.textScalerOf], which typically
907 /// reflects the user-specified text scaling value in the platform's
908 /// accessibility settings. The [TextStyle.fontSize] of the text will be
909 /// adjusted by the [TextScaler] before the text is laid out and rendered.
910 /// {@endtemplate}
911 ///
912 /// The [layout] method must be called after [textScaler] changes as it
913 /// affects the text layout.
914 TextScaler get textScaler => _textScaler;
915 TextScaler _textScaler;
916 set textScaler(TextScaler value) {
917 if (value == _textScaler) {
918 return;
919 }
920 _textScaler = value;
921 markNeedsLayout();
922 _layoutTemplate?.dispose();
923 _layoutTemplate = null;
924 }
925
926 /// The string used to ellipsize overflowing text. Setting this to a non-empty
927 /// string will cause this string to be substituted for the remaining text
928 /// if the text can not fit within the specified maximum width.
929 ///
930 /// Specifically, the ellipsis is applied to the last line before the line
931 /// truncated by [maxLines], if [maxLines] is non-null and that line overflows
932 /// the width constraint, or to the first line that is wider than the width
933 /// constraint, if [maxLines] is null. The width constraint is the `maxWidth`
934 /// passed to [layout].
935 ///
936 /// After this is set, you must call [layout] before the next call to [paint].
937 ///
938 /// The higher layers of the system, such as the [Text] widget, represent
939 /// overflow effects using the [TextOverflow] enum. The
940 /// [TextOverflow.ellipsis] value corresponds to setting this property to
941 /// U+2026 HORIZONTAL ELLIPSIS (…).
942 String? get ellipsis => _ellipsis;
943 String? _ellipsis;
944 set ellipsis(String? value) {
945 assert(value == null || value.isNotEmpty);
946 if (_ellipsis == value) {
947 return;
948 }
949 _ellipsis = value;
950 markNeedsLayout();
951 }
952
953 /// The locale used to select region-specific glyphs.
954 Locale? get locale => _locale;
955 Locale? _locale;
956 set locale(Locale? value) {
957 if (_locale == value) {
958 return;
959 }
960 _locale = value;
961 markNeedsLayout();
962 }
963
964 /// An optional maximum number of lines for the text to span, wrapping if
965 /// necessary.
966 ///
967 /// If the text exceeds the given number of lines, it is truncated such that
968 /// subsequent lines are dropped.
969 ///
970 /// After this is set, you must call [layout] before the next call to [paint].
971 int? get maxLines => _maxLines;
972 int? _maxLines;
973
974 /// The value may be null. If it is not null, then it must be greater than zero.
975 set maxLines(int? value) {
976 assert(value == null || value > 0);
977 if (_maxLines == value) {
978 return;
979 }
980 _maxLines = value;
981 markNeedsLayout();
982 }
983
984 /// {@template flutter.painting.textPainter.strutStyle}
985 /// The strut style to use. Strut style defines the strut, which sets minimum
986 /// vertical layout metrics.
987 ///
988 /// Omitting or providing null will disable strut.
989 ///
990 /// Omitting or providing null for any properties of [StrutStyle] will result in
991 /// default values being used. It is highly recommended to at least specify a
992 /// [StrutStyle.fontSize].
993 ///
994 /// See [StrutStyle] for details.
995 /// {@endtemplate}
996 StrutStyle? get strutStyle => _strutStyle;
997 StrutStyle? _strutStyle;
998 set strutStyle(StrutStyle? value) {
999 if (_strutStyle == value) {
1000 return;
1001 }
1002 _strutStyle = value;
1003 markNeedsLayout();
1004 }
1005
1006 /// {@template flutter.painting.textPainter.textWidthBasis}
1007 /// Defines how to measure the width of the rendered text.
1008 /// {@endtemplate}
1009 TextWidthBasis get textWidthBasis => _textWidthBasis;
1010 TextWidthBasis _textWidthBasis;
1011 set textWidthBasis(TextWidthBasis value) {
1012 if (_textWidthBasis == value) {
1013 return;
1014 }
1015 assert(() {
1016 return _debugNeedsRelayout = true;
1017 }());
1018 _textWidthBasis = value;
1019 }
1020
1021 /// {@macro dart.ui.textHeightBehavior}
1022 TextHeightBehavior? get textHeightBehavior => _textHeightBehavior;
1023 TextHeightBehavior? _textHeightBehavior;
1024 set textHeightBehavior(TextHeightBehavior? value) {
1025 if (_textHeightBehavior == value) {
1026 return;
1027 }
1028 _textHeightBehavior = value;
1029 markNeedsLayout();
1030 }
1031
1032 /// An ordered list of [TextBox]es that bound the positions of the placeholders
1033 /// in the paragraph.
1034 ///
1035 /// Each box corresponds to a [PlaceholderSpan] in the order they were defined
1036 /// in the [InlineSpan] tree.
1037 List<TextBox>? get inlinePlaceholderBoxes {
1038 final _TextPainterLayoutCacheWithOffset? layout = _layoutCache;
1039 if (layout == null) {
1040 return null;
1041 }
1042 final Offset offset = layout.paintOffset;
1043 if (!offset.dx.isFinite || !offset.dy.isFinite) {
1044 return <TextBox>[];
1045 }
1046 final List<TextBox> rawBoxes = layout.inlinePlaceholderBoxes;
1047 if (offset == Offset.zero) {
1048 return rawBoxes;
1049 }
1050 return rawBoxes.map((TextBox box) => _shiftTextBox(box, offset)).toList(growable: false);
1051 }
1052
1053 /// Sets the dimensions of each placeholder in [text].
1054 ///
1055 /// The number of [PlaceholderDimensions] provided should be the same as the
1056 /// number of [PlaceholderSpan]s in text. Passing in an empty or null `value`
1057 /// will do nothing.
1058 ///
1059 /// If [layout] is attempted without setting the placeholder dimensions, the
1060 /// placeholders will be ignored in the text layout and no valid
1061 /// [inlinePlaceholderBoxes] will be returned.
1062 void setPlaceholderDimensions(List<PlaceholderDimensions>? value) {
1063 if (value == null || value.isEmpty || listEquals(value, _placeholderDimensions)) {
1064 return;
1065 }
1066 assert(() {
1067 int placeholderCount = 0;
1068 text!.visitChildren((InlineSpan span) {
1069 if (span is PlaceholderSpan) {
1070 placeholderCount += 1;
1071 }
1072 return value.length >= placeholderCount;
1073 });
1074 return placeholderCount == value.length;
1075 }());
1076 _placeholderDimensions = value;
1077 markNeedsLayout();
1078 }
1079
1080 List<PlaceholderDimensions>? _placeholderDimensions;
1081
1082 ui.ParagraphStyle _createParagraphStyle([TextAlign? textAlignOverride]) {
1083 assert(
1084 textDirection != null,
1085 'TextPainter.textDirection must be set to a non-null value before using the TextPainter.',
1086 );
1087 final TextStyle baseStyle = _text?.style ?? const TextStyle();
1088 return baseStyle.getParagraphStyle(
1089 textAlign: textAlignOverride ?? textAlign,
1090 textDirection: textDirection,
1091 textScaler: textScaler,
1092 maxLines: _maxLines,
1093 textHeightBehavior: _textHeightBehavior,
1094 ellipsis: _ellipsis,
1095 locale: _locale,
1096 strutStyle: _strutStyle,
1097 );
1098 }
1099
1100 ui.Paragraph? _layoutTemplate;
1101 ui.Paragraph _createLayoutTemplate() {
1102 final ui.ParagraphBuilder builder = ui.ParagraphBuilder(
1103 _createParagraphStyle(TextAlign.left),
1104 ); // direction doesn't matter, text is just a space
1105 final ui.TextStyle? textStyle = text?.style?.getTextStyle(textScaler: textScaler);
1106 if (textStyle != null) {
1107 builder.pushStyle(textStyle);
1108 }
1109 builder.addText(' ');
1110 return builder.build()..layout(const ui.ParagraphConstraints(width: double.infinity));
1111 }
1112
1113 ui.Paragraph _getOrCreateLayoutTemplate() => _layoutTemplate ??= _createLayoutTemplate();
1114
1115 /// The height of a space in [text] in logical pixels.
1116 ///
1117 /// Not every line of text in [text] will have this height, but this height
1118 /// is "typical" for text in [text] and useful for sizing other objects
1119 /// relative a typical line of text.
1120 ///
1121 /// Obtaining this value does not require calling [layout].
1122 ///
1123 /// The style of the [text] property is used to determine the font settings
1124 /// that contribute to the [preferredLineHeight]. If [text] is null or if it
1125 /// specifies no styles, the default [TextStyle] values are used (a 10 pixel
1126 /// sans-serif font).
1127 double get preferredLineHeight => _getOrCreateLayoutTemplate().height;
1128
1129 /// The width at which decreasing the width of the text would prevent it from
1130 /// painting itself completely within its bounds.
1131 ///
1132 /// Valid only after [layout] has been called.
1133 double get minIntrinsicWidth {
1134 assert(_debugAssertTextLayoutIsValid);
1135 return _layoutCache!.layout.minIntrinsicLineExtent;
1136 }
1137
1138 /// The width at which increasing the width of the text no longer decreases the height.
1139 ///
1140 /// Valid only after [layout] has been called.
1141 double get maxIntrinsicWidth {
1142 assert(_debugAssertTextLayoutIsValid);
1143 return _layoutCache!.layout.maxIntrinsicLineExtent;
1144 }
1145
1146 /// The horizontal space required to paint this text.
1147 ///
1148 /// Valid only after [layout] has been called.
1149 double get width {
1150 assert(_debugAssertTextLayoutIsValid);
1151 assert(!_debugNeedsRelayout);
1152 return _layoutCache!.contentWidth;
1153 }
1154
1155 /// The vertical space required to paint this text.
1156 ///
1157 /// Valid only after [layout] has been called.
1158 double get height {
1159 assert(_debugAssertTextLayoutIsValid);
1160 return _layoutCache!.layout.height;
1161 }
1162
1163 /// The amount of space required to paint this text.
1164 ///
1165 /// Valid only after [layout] has been called.
1166 Size get size {
1167 assert(_debugAssertTextLayoutIsValid);
1168 assert(!_debugNeedsRelayout);
1169 return Size(width, height);
1170 }
1171
1172 /// Returns the distance from the top of the text to the first baseline of the
1173 /// given type.
1174 ///
1175 /// Valid only after [layout] has been called.
1176 double computeDistanceToActualBaseline(TextBaseline baseline) {
1177 assert(_debugAssertTextLayoutIsValid);
1178 return _layoutCache!.layout.getDistanceToBaseline(baseline);
1179 }
1180
1181 /// Whether any text was truncated or ellipsized.
1182 ///
1183 /// If [maxLines] is not null, this is true if there were more lines to be
1184 /// drawn than the given [maxLines], and thus at least one line was omitted in
1185 /// the output; otherwise it is false.
1186 ///
1187 /// If [maxLines] is null, this is true if [ellipsis] is not the empty string
1188 /// and there was a line that overflowed the `maxWidth` argument passed to
1189 /// [layout]; otherwise it is false.
1190 ///
1191 /// Valid only after [layout] has been called.
1192 bool get didExceedMaxLines {
1193 assert(_debugAssertTextLayoutIsValid);
1194 return _layoutCache!.paragraph.didExceedMaxLines;
1195 }
1196
1197 // Creates a ui.Paragraph using the current configurations in this class and
1198 // assign it to _paragraph.
1199 ui.Paragraph _createParagraph(InlineSpan text) {
1200 final ui.ParagraphBuilder builder = ui.ParagraphBuilder(_createParagraphStyle());
1201 text.build(builder, textScaler: textScaler, dimensions: _placeholderDimensions);
1202 assert(() {
1203 _debugMarkNeedsLayoutCallStack = null;
1204 return true;
1205 }());
1206 _rebuildParagraphForPaint = false;
1207 return builder.build();
1208 }
1209
1210 /// Computes the visual position of the glyphs for painting the text.
1211 ///
1212 /// The text will layout with a width that's as close to its max intrinsic
1213 /// width (or its longest line, if [textWidthBasis] is set to
1214 /// [TextWidthBasis.parent]) as possible while still being greater than or
1215 /// equal to `minWidth` and less than or equal to `maxWidth`.
1216 ///
1217 /// The [text] and [textDirection] properties must be non-null before this is
1218 /// called.
1219 void layout({double minWidth = 0.0, double maxWidth = double.infinity}) {
1220 assert(!maxWidth.isNaN);
1221 assert(!minWidth.isNaN);
1222 assert(() {
1223 _debugNeedsRelayout = false;
1224 return true;
1225 }());
1226
1227 final _TextPainterLayoutCacheWithOffset? cachedLayout = _layoutCache;
1228 if (cachedLayout != null && cachedLayout._resizeToFit(minWidth, maxWidth, textWidthBasis)) {
1229 return;
1230 }
1231
1232 final InlineSpan? text = this.text;
1233 if (text == null) {
1234 throw StateError(
1235 'TextPainter.text must be set to a non-null value before using the TextPainter.',
1236 );
1237 }
1238 final TextDirection? textDirection = this.textDirection;
1239 if (textDirection == null) {
1240 throw StateError(
1241 'TextPainter.textDirection must be set to a non-null value before using the TextPainter.',
1242 );
1243 }
1244
1245 final double paintOffsetAlignment = _computePaintOffsetFraction(textAlign, textDirection);
1246 // Try to avoid laying out the paragraph with maxWidth=double.infinity
1247 // when the text is not left-aligned, so we don't have to deal with an
1248 // infinite paint offset.
1249 final bool adjustMaxWidth = !maxWidth.isFinite && paintOffsetAlignment != 0;
1250 final double? adjustedMaxWidth =
1251 !adjustMaxWidth ? maxWidth : cachedLayout?.layout.maxIntrinsicLineExtent;
1252 final double layoutMaxWidth = adjustedMaxWidth ?? maxWidth;
1253
1254 // Only rebuild the paragraph when there're layout changes, even when
1255 // `_rebuildParagraphForPaint` is true. It's best to not eagerly rebuild
1256 // the paragraph to avoid the extra work, because:
1257 // 1. the text color could change again before `paint` is called (so one of
1258 // the paragraph rebuilds is unnecessary)
1259 // 2. the user could be measuring the text layout so `paint` will never be
1260 // called.
1261 final ui.Paragraph paragraph =
1262 (cachedLayout?.paragraph ?? _createParagraph(text))
1263 ..layout(ui.ParagraphConstraints(width: layoutMaxWidth));
1264 final _TextLayout layout = _TextLayout._(paragraph, textDirection, this);
1265 final double contentWidth = layout._contentWidthFor(minWidth, maxWidth, textWidthBasis);
1266
1267 final _TextPainterLayoutCacheWithOffset newLayoutCache;
1268 // Call layout again if newLayoutCache had an infinite paint offset.
1269 // This is not as expensive as it seems, line breaking is relatively cheap
1270 // as compared to shaping.
1271 if (adjustedMaxWidth == null && minWidth.isFinite) {
1272 assert(maxWidth.isInfinite);
1273 final double newInputWidth = layout.maxIntrinsicLineExtent;
1274 paragraph.layout(ui.ParagraphConstraints(width: newInputWidth));
1275 newLayoutCache = _TextPainterLayoutCacheWithOffset(
1276 layout,
1277 paintOffsetAlignment,
1278 newInputWidth,
1279 contentWidth,
1280 );
1281 } else {
1282 newLayoutCache = _TextPainterLayoutCacheWithOffset(
1283 layout,
1284 paintOffsetAlignment,
1285 layoutMaxWidth,
1286 contentWidth,
1287 );
1288 }
1289 _layoutCache = newLayoutCache;
1290 }
1291
1292 /// Paints the text onto the given canvas at the given offset.
1293 ///
1294 /// Valid only after [layout] has been called.
1295 ///
1296 /// If you cannot see the text being painted, check that your text color does
1297 /// not conflict with the background on which you are drawing. The default
1298 /// text color is white (to contrast with the default black background color),
1299 /// so if you are writing an application with a white background, the text
1300 /// will not be visible by default.
1301 ///
1302 /// To set the text style, specify a [TextStyle] when creating the [TextSpan]
1303 /// that you pass to the [TextPainter] constructor or to the [text] property.
1304 void paint(Canvas canvas, Offset offset) {
1305 final _TextPainterLayoutCacheWithOffset? layoutCache = _layoutCache;
1306 if (layoutCache == null) {
1307 throw StateError(
1308 'TextPainter.paint called when text geometry was not yet calculated.\n'
1309 'Please call layout() before paint() to position the text before painting it.',
1310 );
1311 }
1312
1313 if (!layoutCache.paintOffset.dx.isFinite || !layoutCache.paintOffset.dy.isFinite) {
1314 return;
1315 }
1316
1317 if (_rebuildParagraphForPaint) {
1318 Size? debugSize;
1319 assert(() {
1320 debugSize = size;
1321 return true;
1322 }());
1323
1324 final ui.Paragraph paragraph = layoutCache.paragraph;
1325 // Unfortunately even if we know that there is only paint changes, there's
1326 // no API to only make those updates so the paragraph has to be recreated
1327 // and re-laid out.
1328 assert(!layoutCache.layoutMaxWidth.isNaN);
1329 layoutCache.layout._paragraph = _createParagraph(text!)
1330 ..layout(ui.ParagraphConstraints(width: layoutCache.layoutMaxWidth));
1331 assert(paragraph.width == layoutCache.layout._paragraph.width);
1332 paragraph.dispose();
1333 assert(debugSize == size);
1334 }
1335 assert(!_rebuildParagraphForPaint);
1336 canvas.drawParagraph(layoutCache.paragraph, offset + layoutCache.paintOffset);
1337 }
1338
1339 // Returns true if value falls in the valid range of the UTF16 encoding.
1340 static bool _isUTF16(int value) {
1341 return value >= 0x0 && value <= 0xFFFFF;
1342 }
1343
1344 /// Returns true iff the given value is a valid UTF-16 high (first) surrogate.
1345 /// The value must be a UTF-16 code unit, meaning it must be in the range
1346 /// 0x0000-0xFFFF.
1347 ///
1348 /// See also:
1349 /// * https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF
1350 /// * [isLowSurrogate], which checks the same thing for low (second)
1351 /// surrogates.
1352 static bool isHighSurrogate(int value) {
1353 assert(_isUTF16(value));
1354 return value & 0xFC00 == 0xD800;
1355 }
1356
1357 /// Returns true iff the given value is a valid UTF-16 low (second) surrogate.
1358 /// The value must be a UTF-16 code unit, meaning it must be in the range
1359 /// 0x0000-0xFFFF.
1360 ///
1361 /// See also:
1362 /// * https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF
1363 /// * [isHighSurrogate], which checks the same thing for high (first)
1364 /// surrogates.
1365 static bool isLowSurrogate(int value) {
1366 assert(_isUTF16(value));
1367 return value & 0xFC00 == 0xDC00;
1368 }
1369
1370 /// Returns the closest offset after `offset` at which the input cursor can be
1371 /// positioned.
1372 int? getOffsetAfter(int offset) {
1373 final int? nextCodeUnit = _text!.codeUnitAt(offset);
1374 if (nextCodeUnit == null) {
1375 return null;
1376 }
1377 // TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404).
1378 return isHighSurrogate(nextCodeUnit) ? offset + 2 : offset + 1;
1379 }
1380
1381 /// Returns the closest offset before `offset` at which the input cursor can
1382 /// be positioned.
1383 int? getOffsetBefore(int offset) {
1384 final int? prevCodeUnit = _text!.codeUnitAt(offset - 1);
1385 if (prevCodeUnit == null) {
1386 return null;
1387 }
1388 // TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404).
1389 return isLowSurrogate(prevCodeUnit) ? offset - 2 : offset - 1;
1390 }
1391
1392 static double _computePaintOffsetFraction(TextAlign textAlign, TextDirection textDirection) {
1393 return switch ((textAlign, textDirection)) {
1394 (TextAlign.left, _) => 0.0,
1395 (TextAlign.right, _) => 1.0,
1396 (TextAlign.center, _) => 0.5,
1397 (TextAlign.start || TextAlign.justify, TextDirection.ltr) => 0.0,
1398 (TextAlign.start || TextAlign.justify, TextDirection.rtl) => 1.0,
1399 (TextAlign.end, TextDirection.ltr) => 1.0,
1400 (TextAlign.end, TextDirection.rtl) => 0.0,
1401 };
1402 }
1403
1404 /// Returns the offset at which to paint the caret.
1405 ///
1406 /// Valid only after [layout] has been called.
1407 Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
1408 final _TextPainterLayoutCacheWithOffset layoutCache = _layoutCache!;
1409 final _LineCaretMetrics? caretMetrics = _computeCaretMetrics(position);
1410
1411 if (caretMetrics == null) {
1412 final double paintOffsetAlignment = _computePaintOffsetFraction(textAlign, textDirection!);
1413 // The full width is not (width - caretPrototype.width), because
1414 // RenderEditable reserves cursor width on the right. Ideally this
1415 // should be handled by RenderEditable instead.
1416 final double dx =
1417 paintOffsetAlignment == 0 ? 0 : paintOffsetAlignment * layoutCache.contentWidth;
1418 return Offset(dx, 0.0);
1419 }
1420
1421 final Offset rawOffset = switch (caretMetrics) {
1422 _LineCaretMetrics(writingDirection: TextDirection.ltr, :final Offset offset) => offset,
1423 _LineCaretMetrics(writingDirection: TextDirection.rtl, :final Offset offset) => Offset(
1424 offset.dx - caretPrototype.width,
1425 offset.dy,
1426 ),
1427 };
1428 // If offset.dx is outside of the advertised content area, then the associated
1429 // glyph belongs to a trailing whitespace character. Ideally the behavior
1430 // should be handled by higher-level implementations (for instance,
1431 // RenderEditable reserves width for showing the caret, it's best to handle
1432 // the clamping there).
1433 final double adjustedDx = clampDouble(
1434 rawOffset.dx + layoutCache.paintOffset.dx,
1435 0,
1436 layoutCache.contentWidth,
1437 );
1438 return Offset(adjustedDx, rawOffset.dy + layoutCache.paintOffset.dy);
1439 }
1440
1441 /// {@template flutter.painting.textPainter.getFullHeightForCaret}
1442 /// Returns the strut bounded height of the glyph at the given `position`.
1443 /// {@endtemplate}
1444 ///
1445 /// Valid only after [layout] has been called.
1446 double getFullHeightForCaret(TextPosition position, Rect caretPrototype) {
1447 // The if condition is derived from
1448 // https://github.com/google/skia/blob/0086a17e0d4cc676cf88cae671ba5ee967eb7241/modules/skparagraph/src/TextLine.cpp#L1244-L1246
1449 // which is set here:
1450 // https://github.com/flutter/engine/blob/a821b8790c9fd0e095013cd5bd1f20273bc1ee47/third_party/txt/src/skia/paragraph_builder_skia.cc#L134
1451 if (strutStyle == null || strutStyle == StrutStyle.disabled || strutStyle?.fontSize == 0.0) {
1452 final double? heightFromCaretMetrics = _computeCaretMetrics(position)?.height;
1453 if (heightFromCaretMetrics != null) {
1454 return heightFromCaretMetrics;
1455 }
1456 }
1457 final TextBox textBox =
1458 _getOrCreateLayoutTemplate()
1459 .getBoxesForRange(0, 1, boxHeightStyle: ui.BoxHeightStyle.strut)
1460 .single;
1461 return textBox.toRect().height;
1462 }
1463
1464 bool _isNewlineAtOffset(int offset) =>
1465 0 <= offset &&
1466 offset < plainText.length &&
1467 WordBoundary._isNewline(plainText.codeUnitAt(offset));
1468
1469 // Cached caret metrics. This allows multiple invokes of [getOffsetForCaret] and
1470 // [getFullHeightForCaret] in a row without performing redundant and expensive
1471 // get rect calls to the paragraph.
1472 //
1473 // The cache implementation assumes there's only one cursor at any given time.
1474 late _LineCaretMetrics _caretMetrics;
1475
1476 // This function returns the caret's offset and height for the given
1477 // `position` in the text, or null if the paragraph is empty.
1478 //
1479 // For a TextPosition, typically when its TextAffinity is downstream, the
1480 // corresponding I-beam caret is anchored to the leading edge of the character
1481 // at `offset` in the text. When the TextAffinity is upstream, the I-beam is
1482 // then anchored to the trailing edge of the preceding character, except for a
1483 // few edge cases:
1484 //
1485 // 1. empty paragraph: this method returns null and the caller handles this
1486 // case.
1487 //
1488 // 2. (textLength, downstream), the end-of-text caret when the text is not
1489 // empty: it's placed next to the trailing edge of the last line of the
1490 // text, in case the text and its last bidi run have different writing
1491 // directions. See the `_computeEndOfTextCaretAnchorOffset` method for more
1492 // details.
1493 //
1494 // 3. (0, upstream), which isn't a valid position, but it's not a conventional
1495 // "invalid" caret location either (the offset isn't negative). For
1496 // historical reasons, this is treated as (0, downstream).
1497 //
1498 // 4. (x, upstream) where x - 1 points to a line break character. The caret
1499 // should be displayed at the beginning of the newline instead of at the
1500 // end of the previous line. Converts the location to (x, downstream). The
1501 // choice we makes in 5. allows us to still check (x - 1) in case x points
1502 // to a multi-code-unit character.
1503 //
1504 // 5. (x, downstream || upstream), where x points to a multi-code-unit
1505 // character. There's no perfect caret placement in this case. Here we chose
1506 // to draw the caret at the location that makes the most sense when the
1507 // user wants to backspace (which also means it's left-arrow-key-biased):
1508 //
1509 // * downstream: show the caret at the leading edge of the character only if
1510 // x points to the start of the grapheme. Otherwise show the caret at the
1511 // leading edge of the next logical character.
1512 // * upstream: show the caret at the trailing edge of the previous character
1513 // only if x points to the start of the grapheme. Otherwise place the
1514 // caret at the trailing edge of the character.
1515 _LineCaretMetrics? _computeCaretMetrics(TextPosition position) {
1516 assert(_debugAssertTextLayoutIsValid);
1517 assert(!_debugNeedsRelayout);
1518
1519 final _TextPainterLayoutCacheWithOffset cachedLayout = _layoutCache!;
1520 // If nothing is laid out, top start is the only reasonable place to place
1521 // the cursor.
1522 if (cachedLayout.paragraph.numberOfLines < 1) {
1523 // TODO(LongCatIsLooong): assert when an invalid position is given.
1524 return null;
1525 }
1526
1527 final (int offset, bool anchorToLeadingEdge) = switch (position) {
1528 TextPosition(offset: 0) => (
1529 0,
1530 true,
1531 ), // As a special case, always anchor to the leading edge of the first grapheme regardless of the affinity.
1532 TextPosition(:final int offset, affinity: TextAffinity.downstream) => (offset, true),
1533 TextPosition(:final int offset, affinity: TextAffinity.upstream)
1534 when _isNewlineAtOffset(offset - 1) =>
1535 (offset, true),
1536 TextPosition(:final int offset, affinity: TextAffinity.upstream) => (offset - 1, false),
1537 };
1538
1539 final int caretPositionCacheKey = anchorToLeadingEdge ? offset : -offset - 1;
1540 if (caretPositionCacheKey == cachedLayout._previousCaretPositionKey) {
1541 return _caretMetrics;
1542 }
1543
1544 final ui.GlyphInfo? glyphInfo = cachedLayout.paragraph.getGlyphInfoAt(offset);
1545
1546 if (glyphInfo == null) {
1547 // If the glyph isn't laid out, then the position points to a character
1548 // that is not laid out (the part of text is invisible due to maxLines or
1549 // infinite paragraph x offset). Use the EOT caret.
1550 // TODO(LongCatIsLooong): assert when an invalid position is given.
1551 final ui.Paragraph template = _getOrCreateLayoutTemplate();
1552 assert(template.numberOfLines == 1);
1553 final double baselineOffset = template.getLineMetricsAt(0)!.baseline;
1554 return cachedLayout.layout._endOfTextCaretMetrics.shift(Offset(0.0, -baselineOffset));
1555 }
1556
1557 final TextRange graphemeRange = glyphInfo.graphemeClusterCodeUnitRange;
1558
1559 // Works around a SkParagraph bug (https://github.com/flutter/flutter/issues/120836#issuecomment-1937343854):
1560 // placeholders with a size of (0, 0) always have a rect of Rect.zero and a
1561 // range of (0, 0).
1562 if (graphemeRange.isCollapsed) {
1563 assert(graphemeRange.start == 0);
1564 return _computeCaretMetrics(TextPosition(offset: offset + 1));
1565 }
1566 if (anchorToLeadingEdge && graphemeRange.start != offset) {
1567 assert(graphemeRange.end > graphemeRange.start + 1);
1568 // Addresses the case where `offset` points to a multi-code-unit grapheme
1569 // that doesn't start at `offset`.
1570 return _computeCaretMetrics(TextPosition(offset: graphemeRange.end));
1571 }
1572
1573 final _LineCaretMetrics metrics;
1574 final List<TextBox> boxes = cachedLayout.paragraph.getBoxesForRange(
1575 graphemeRange.start,
1576 graphemeRange.end,
1577 boxHeightStyle: ui.BoxHeightStyle.strut,
1578 );
1579
1580 final bool anchorToLeft = switch (glyphInfo.writingDirection) {
1581 TextDirection.ltr => anchorToLeadingEdge,
1582 TextDirection.rtl => !anchorToLeadingEdge,
1583 };
1584 final TextBox box = anchorToLeft ? boxes.first : boxes.last;
1585 metrics = _LineCaretMetrics(
1586 offset: Offset(anchorToLeft ? box.left : box.right, box.top),
1587 writingDirection: box.direction,
1588 height: box.bottom - box.top,
1589 );
1590
1591 cachedLayout._previousCaretPositionKey = caretPositionCacheKey;
1592 return _caretMetrics = metrics;
1593 }
1594
1595 /// Returns a list of rects that bound the given selection.
1596 ///
1597 /// The [selection] must be a valid range (with [TextSelection.isValid] true).
1598 ///
1599 /// The [boxHeightStyle] and [boxWidthStyle] arguments may be used to select
1600 /// the shape of the [TextBox]s. These properties default to
1601 /// [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively.
1602 ///
1603 /// A given selection might have more than one rect if this text painter
1604 /// contains bidirectional text because logically contiguous text might not be
1605 /// visually contiguous.
1606 ///
1607 /// Leading or trailing newline characters will be represented by zero-width
1608 /// `TextBox`es.
1609 ///
1610 /// The method only returns `TextBox`es of glyphs that are entirely enclosed by
1611 /// the given `selection`: a multi-code-unit glyph will be excluded if only
1612 /// part of its code units are in `selection`.
1613 List<TextBox> getBoxesForSelection(
1614 TextSelection selection, {
1615 ui.BoxHeightStyle boxHeightStyle = ui.BoxHeightStyle.tight,
1616 ui.BoxWidthStyle boxWidthStyle = ui.BoxWidthStyle.tight,
1617 }) {
1618 assert(_debugAssertTextLayoutIsValid);
1619 assert(selection.isValid);
1620 assert(!_debugNeedsRelayout);
1621 final _TextPainterLayoutCacheWithOffset cachedLayout = _layoutCache!;
1622 final Offset offset = cachedLayout.paintOffset;
1623 if (!offset.dx.isFinite || !offset.dy.isFinite) {
1624 return <TextBox>[];
1625 }
1626 final List<TextBox> boxes = cachedLayout.paragraph.getBoxesForRange(
1627 selection.start,
1628 selection.end,
1629 boxHeightStyle: boxHeightStyle,
1630 boxWidthStyle: boxWidthStyle,
1631 );
1632 return offset == Offset.zero
1633 ? boxes
1634 : boxes.map((TextBox box) => _shiftTextBox(box, offset)).toList(growable: false);
1635 }
1636
1637 /// Returns the [GlyphInfo] of the glyph closest to the given `offset` in the
1638 /// paragraph coordinate system, or null if the text is empty, or is entirely
1639 /// clipped or ellipsized away.
1640 ///
1641 /// This method first finds the line closest to `offset.dy`, and then returns
1642 /// the [GlyphInfo] of the closest glyph(s) within that line.
1643 ui.GlyphInfo? getClosestGlyphForOffset(Offset offset) {
1644 assert(_debugAssertTextLayoutIsValid);
1645 assert(!_debugNeedsRelayout);
1646 final _TextPainterLayoutCacheWithOffset cachedLayout = _layoutCache!;
1647 final ui.GlyphInfo? rawGlyphInfo = cachedLayout.paragraph.getClosestGlyphInfoForOffset(
1648 offset - cachedLayout.paintOffset,
1649 );
1650 if (rawGlyphInfo == null || cachedLayout.paintOffset == Offset.zero) {
1651 return rawGlyphInfo;
1652 }
1653 return ui.GlyphInfo(
1654 rawGlyphInfo.graphemeClusterLayoutBounds.shift(cachedLayout.paintOffset),
1655 rawGlyphInfo.graphemeClusterCodeUnitRange,
1656 rawGlyphInfo.writingDirection,
1657 );
1658 }
1659
1660 /// Returns the closest position within the text for the given pixel offset.
1661 TextPosition getPositionForOffset(Offset offset) {
1662 assert(_debugAssertTextLayoutIsValid);
1663 assert(!_debugNeedsRelayout);
1664 final _TextPainterLayoutCacheWithOffset cachedLayout = _layoutCache!;
1665 return cachedLayout.paragraph.getPositionForOffset(offset - cachedLayout.paintOffset);
1666 }
1667
1668 /// {@template flutter.painting.TextPainter.getWordBoundary}
1669 /// Returns the text range of the word at the given offset. Characters not
1670 /// part of a word, such as spaces, symbols, and punctuation, have word breaks
1671 /// on both sides. In such cases, this method will return a text range that
1672 /// contains the given text position.
1673 ///
1674 /// Word boundaries are defined more precisely in Unicode Standard Annex #29
1675 /// <http://www.unicode.org/reports/tr29/#Word_Boundaries>.
1676 /// {@endtemplate}
1677 TextRange getWordBoundary(TextPosition position) {
1678 assert(_debugAssertTextLayoutIsValid);
1679 return _layoutCache!.paragraph.getWordBoundary(position);
1680 }
1681
1682 /// {@template flutter.painting.TextPainter.wordBoundaries}
1683 /// Returns a [TextBoundary] that can be used to perform word boundary analysis
1684 /// on the current [text].
1685 ///
1686 /// This [TextBoundary] uses word boundary rules defined in [Unicode Standard
1687 /// Annex #29](http://www.unicode.org/reports/tr29/#Word_Boundaries).
1688 /// {@endtemplate}
1689 ///
1690 /// Currently word boundary analysis can only be performed after [layout]
1691 /// has been called.
1692 WordBoundary get wordBoundaries => WordBoundary._(text!, _layoutCache!.paragraph);
1693
1694 /// Returns the text range of the line at the given offset.
1695 ///
1696 /// The newline (if any) is not returned as part of the range.
1697 TextRange getLineBoundary(TextPosition position) {
1698 assert(_debugAssertTextLayoutIsValid);
1699 return _layoutCache!.paragraph.getLineBoundary(position);
1700 }
1701
1702 static ui.LineMetrics _shiftLineMetrics(ui.LineMetrics metrics, Offset offset) {
1703 assert(offset.dx.isFinite);
1704 assert(offset.dy.isFinite);
1705 return ui.LineMetrics(
1706 hardBreak: metrics.hardBreak,
1707 ascent: metrics.ascent,
1708 descent: metrics.descent,
1709 unscaledAscent: metrics.unscaledAscent,
1710 height: metrics.height,
1711 width: metrics.width,
1712 left: metrics.left + offset.dx,
1713 baseline: metrics.baseline + offset.dy,
1714 lineNumber: metrics.lineNumber,
1715 );
1716 }
1717
1718 static TextBox _shiftTextBox(TextBox box, Offset offset) {
1719 assert(offset.dx.isFinite);
1720 assert(offset.dy.isFinite);
1721 return TextBox.fromLTRBD(
1722 box.left + offset.dx,
1723 box.top + offset.dy,
1724 box.right + offset.dx,
1725 box.bottom + offset.dy,
1726 box.direction,
1727 );
1728 }
1729
1730 /// Returns the full list of [LineMetrics] that describe in detail the various
1731 /// metrics of each laid out line.
1732 ///
1733 /// The [LineMetrics] list is presented in the order of the lines they represent.
1734 /// For example, the first line is in the zeroth index.
1735 ///
1736 /// [LineMetrics] contains measurements such as ascent, descent, baseline, and
1737 /// width for the line as a whole, and may be useful for aligning additional
1738 /// widgets to a particular line.
1739 ///
1740 /// Valid only after [layout] has been called.
1741 List<ui.LineMetrics> computeLineMetrics() {
1742 assert(_debugAssertTextLayoutIsValid);
1743 assert(!_debugNeedsRelayout);
1744 final _TextPainterLayoutCacheWithOffset layout = _layoutCache!;
1745 final Offset offset = layout.paintOffset;
1746 if (!offset.dx.isFinite || !offset.dy.isFinite) {
1747 return const <ui.LineMetrics>[];
1748 }
1749 final List<ui.LineMetrics> rawMetrics = layout.lineMetrics;
1750 return offset == Offset.zero
1751 ? rawMetrics
1752 : rawMetrics
1753 .map((ui.LineMetrics metrics) => _shiftLineMetrics(metrics, offset))
1754 .toList(growable: false);
1755 }
1756
1757 bool _disposed = false;
1758
1759 /// Whether this object has been disposed or not.
1760 ///
1761 /// Only for use when asserts are enabled.
1762 bool get debugDisposed {
1763 bool? disposed;
1764 assert(() {
1765 disposed = _disposed;
1766 return true;
1767 }());
1768 return disposed ?? (throw StateError('debugDisposed only available when asserts are on.'));
1769 }
1770
1771 /// Releases the resources associated with this painter.
1772 ///
1773 /// After disposal this painter is unusable.
1774 void dispose() {
1775 assert(!debugDisposed);
1776 assert(() {
1777 _disposed = true;
1778 return true;
1779 }());
1780 assert(debugMaybeDispatchDisposed(this));
1781 _layoutTemplate?.dispose();
1782 _layoutTemplate = null;
1783 _layoutCache?.paragraph.dispose();
1784 _layoutCache = null;
1785 _text = null;
1786 }
1787}
1788

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com