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'; |
6 | library; |
7 | |
8 | import 'dart:math' show max; |
9 | import '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 | |
23 | import 'package:flutter/foundation.dart'; |
24 | import 'package:flutter/services.dart'; |
25 | |
26 | import 'basic_types.dart'; |
27 | import 'inline_span.dart'; |
28 | import 'placeholder_span.dart'; |
29 | import 'strut_style.dart'; |
30 | import 'text_scaler.dart'; |
31 | import 'text_span.dart'; |
32 | import 'text_style.dart'; |
33 | |
34 | export 'dart:ui' show LineMetrics; |
35 | export '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). |
41 | const 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. |
47 | enum 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 |
74 | class 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. |
152 | enum 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. |
177 | class 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 | |
271 | class _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 | |
295 | class _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. |
426 | class _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. |
534 | class _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. |
589 | class 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 |
Definitions
- kDefaultFontSize
- TextOverflow
- PlaceholderDimensions
- PlaceholderDimensions
- ==
- hashCode
- toString
- TextWidthBasis
- WordBoundary
- _
- getTextBoundaryAt
- _codePointFromSurrogates
- _codePointAt
- _isNewline
- _skipSpacesAndPunctuations
- _UntilTextBoundary
- _UntilTextBoundary
- getLeadingTextBoundaryAt
- getTrailingTextBoundaryAt
- _TextLayout
- _
- debugDisposed
- width
- height
- minIntrinsicLineExtent
- maxIntrinsicLineExtent
- longestLine
- getDistanceToBaseline
- _computeEndOfTextCaretAnchorOffset
- _contentWidthFor
- _TextPainterLayoutCacheWithOffset
- _TextPainterLayoutCacheWithOffset
- paintOffset
- paragraph
- _resizeToFit
- inlinePlaceholderBoxes
- lineMetrics
- _LineCaretMetrics
- _LineCaretMetrics
- shift
- TextPainter
- TextPainter
- computeWidth
- computeMaxIntrinsicWidth
- _debugAssertTextLayoutIsValid
- markNeedsLayout
- text
- text
- plainText
- textAlign
- textAlign
- textDirection
- textDirection
- textScaleFactor
- textScaleFactor
- textScaler
- textScaler
- ellipsis
- ellipsis
- locale
- locale
- maxLines
- maxLines
- strutStyle
- strutStyle
- textWidthBasis
- textWidthBasis
- textHeightBehavior
- textHeightBehavior
- inlinePlaceholderBoxes
- setPlaceholderDimensions
- _createParagraphStyle
- _createLayoutTemplate
- _getOrCreateLayoutTemplate
- preferredLineHeight
- minIntrinsicWidth
- maxIntrinsicWidth
- width
- height
- size
- computeDistanceToActualBaseline
- didExceedMaxLines
- _createParagraph
- layout
- paint
- _isUTF16
- isHighSurrogate
- isLowSurrogate
- getOffsetAfter
- getOffsetBefore
- _computePaintOffsetFraction
- getOffsetForCaret
- getFullHeightForCaret
- _isNewlineAtOffset
- _computeCaretMetrics
- getBoxesForSelection
- getClosestGlyphForOffset
- getPositionForOffset
- getWordBoundary
- wordBoundaries
- getLineBoundary
- _shiftLineMetrics
- _shiftTextBox
- computeLineMetrics
- debugDisposed
Learn more about Flutter for embedded and desktop on industrialflutter.com