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