| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:math' show max; |
| import 'dart:ui' as ui show |
| BoxHeightStyle, |
| BoxWidthStyle, |
| GlyphInfo, |
| LineMetrics, |
| Paragraph, |
| ParagraphBuilder, |
| ParagraphConstraints, |
| ParagraphStyle, |
| PlaceholderAlignment, |
| TextStyle; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/services.dart'; |
| |
| import 'basic_types.dart'; |
| import 'inline_span.dart'; |
| import 'placeholder_span.dart'; |
| import 'strut_style.dart'; |
| import 'text_scaler.dart'; |
| import 'text_span.dart'; |
| import 'text_style.dart'; |
| |
| export 'dart:ui' show LineMetrics; |
| export 'package:flutter/services.dart' show TextRange, TextSelection; |
| |
| /// The default font size if none is specified. |
| /// |
| /// This should be kept in sync with the defaults set in the engine (e.g., |
| /// LibTxt's text_style.h, paragraph_style.h). |
| const double kDefaultFontSize = 14.0; |
| |
| /// How overflowing text should be handled. |
| /// |
| /// A [TextOverflow] can be passed to [Text] and [RichText] via their |
| /// [Text.overflow] and [RichText.overflow] properties respectively. |
| enum TextOverflow { |
| /// Clip the overflowing text to fix its container. |
| clip, |
| |
| /// Fade the overflowing text to transparent. |
| fade, |
| |
| /// Use an ellipsis to indicate that the text has overflowed. |
| ellipsis, |
| |
| /// Render overflowing text outside of its container. |
| visible, |
| } |
| |
| /// Holds the [Size] and baseline required to represent the dimensions of |
| /// a placeholder in text. |
| /// |
| /// Placeholders specify an empty space in the text layout, which is used |
| /// to later render arbitrary inline widgets into defined by a [WidgetSpan]. |
| /// |
| /// See also: |
| /// |
| /// * [WidgetSpan], a subclass of [InlineSpan] and [PlaceholderSpan] that |
| /// represents an inline widget embedded within text. The space this |
| /// widget takes is indicated by a placeholder. |
| /// * [RichText], a text widget that supports text inline widgets. |
| @immutable |
| class PlaceholderDimensions { |
| /// Constructs a [PlaceholderDimensions] with the specified parameters. |
| /// |
| /// The `size` and `alignment` are required as a placeholder's dimensions |
| /// require at least `size` and `alignment` to be fully defined. |
| const PlaceholderDimensions({ |
| required this.size, |
| required this.alignment, |
| this.baseline, |
| this.baselineOffset, |
| }); |
| |
| /// A constant representing an empty placeholder. |
| static const PlaceholderDimensions empty = PlaceholderDimensions(size: Size.zero, alignment: ui.PlaceholderAlignment.bottom); |
| |
| /// Width and height dimensions of the placeholder. |
| final Size size; |
| |
| /// How to align the placeholder with the text. |
| /// |
| /// See also: |
| /// |
| /// * [baseline], the baseline to align to when using |
| /// [dart:ui.PlaceholderAlignment.baseline], |
| /// [dart:ui.PlaceholderAlignment.aboveBaseline], |
| /// or [dart:ui.PlaceholderAlignment.belowBaseline]. |
| /// * [baselineOffset], the distance of the alphabetic baseline from the upper |
| /// edge of the placeholder. |
| final ui.PlaceholderAlignment alignment; |
| |
| /// Distance of the [baseline] from the upper edge of the placeholder. |
| /// |
| /// Only used when [alignment] is [ui.PlaceholderAlignment.baseline]. |
| final double? baselineOffset; |
| |
| /// The [TextBaseline] to align to. Used with: |
| /// |
| /// * [ui.PlaceholderAlignment.baseline] |
| /// * [ui.PlaceholderAlignment.aboveBaseline] |
| /// * [ui.PlaceholderAlignment.belowBaseline] |
| /// * [ui.PlaceholderAlignment.middle] |
| final TextBaseline? baseline; |
| |
| @override |
| bool operator ==(Object other) { |
| if (identical(this, other)) { |
| return true; |
| } |
| return other is PlaceholderDimensions |
| && other.size == size |
| && other.alignment == alignment |
| && other.baseline == baseline |
| && other.baselineOffset == baselineOffset; |
| } |
| |
| @override |
| int get hashCode => Object.hash(size, alignment, baseline, baselineOffset); |
| |
| @override |
| String toString() { |
| return switch (alignment) { |
| ui.PlaceholderAlignment.top || |
| ui.PlaceholderAlignment.bottom || |
| ui.PlaceholderAlignment.middle || |
| ui.PlaceholderAlignment.aboveBaseline || |
| ui.PlaceholderAlignment.belowBaseline => 'PlaceholderDimensions($size, $alignment)', |
| ui.PlaceholderAlignment.baseline => 'PlaceholderDimensions($size, $alignment($baselineOffset from top))', |
| }; |
| } |
| } |
| |
| /// The different ways of measuring the width of one or more lines of text. |
| /// |
| /// See [Text.textWidthBasis], for example. |
| enum TextWidthBasis { |
| /// multiline text will take up the full width given by the parent. For single |
| /// line text, only the minimum amount of width needed to contain the text |
| /// will be used. A common use case for this is a standard series of |
| /// paragraphs. |
| parent, |
| |
| /// The width will be exactly enough to contain the longest line and no |
| /// longer. A common use case for this is chat bubbles. |
| longestLine, |
| } |
| |
| /// A [TextBoundary] subclass for locating word breaks. |
| /// |
| /// The underlying implementation uses [UAX #29](https://unicode.org/reports/tr29/) |
| /// defined default word boundaries. |
| /// |
| /// The default word break rules can be tailored to meet the requirements of |
| /// different use cases. For instance, the default rule set keeps horizontal |
| /// whitespaces together as a single word, which may not make sense in a |
| /// word-counting context -- "hello world" counts as 3 words instead of 2. |
| /// An example is the [moveByWordBoundary] variant, which is a tailored |
| /// word-break locator that more closely matches the default behavior of most |
| /// platforms and editors when it comes to handling text editing keyboard |
| /// shortcuts that move or delete word by word. |
| class WordBoundary extends TextBoundary { |
| /// Creates a [WordBoundary] with the text and layout information. |
| WordBoundary._(this._text, this._paragraph); |
| |
| final InlineSpan _text; |
| final ui.Paragraph _paragraph; |
| |
| @override |
| TextRange getTextBoundaryAt(int position) => _paragraph.getWordBoundary(TextPosition(offset: max(position, 0))); |
| |
| // Combines two UTF-16 code units (high surrogate + low surrogate) into a |
| // single code point that represents a supplementary character. |
| static int _codePointFromSurrogates(int highSurrogate, int lowSurrogate) { |
| assert( |
| TextPainter.isHighSurrogate(highSurrogate), |
| 'U+${highSurrogate.toRadixString(16).toUpperCase().padLeft(4, "0")}) is not a high surrogate.', |
| ); |
| assert( |
| TextPainter.isLowSurrogate(lowSurrogate), |
| 'U+${lowSurrogate.toRadixString(16).toUpperCase().padLeft(4, "0")}) is not a low surrogate.', |
| ); |
| const int base = 0x010000 - (0xD800 << 10) - 0xDC00; |
| return (highSurrogate << 10) + lowSurrogate + base; |
| } |
| |
| // The Runes class does not provide random access with a code unit offset. |
| int? _codePointAt(int index) { |
| final int? codeUnitAtIndex = _text.codeUnitAt(index); |
| if (codeUnitAtIndex == null) { |
| return null; |
| } |
| return switch (codeUnitAtIndex & 0xFC00) { |
| 0xD800 => _codePointFromSurrogates(codeUnitAtIndex, _text.codeUnitAt(index + 1)!), |
| 0xDC00 => _codePointFromSurrogates(_text.codeUnitAt(index - 1)!, codeUnitAtIndex), |
| _ => codeUnitAtIndex, |
| }; |
| } |
| |
| static bool _isNewline(int codePoint) { |
| // Carriage Return is not treated as a hard line break. |
| return switch (codePoint) { |
| 0x000A || // Line Feed |
| 0x0085 || // New Line |
| 0x000B || // Form Feed |
| 0x000C || // Vertical Feed |
| 0x2028 || // Line Separator |
| 0x2029 => true, // Paragraph Separator |
| _ => false, |
| }; |
| } |
| |
| bool _skipSpacesAndPunctuations(int offset, bool forward) { |
| // Use code point since some punctuations are supplementary characters. |
| // "inner" here refers to the code unit that's before the break in the |
| // search direction (`forward`). |
| final int? innerCodePoint = _codePointAt(forward ? offset - 1 : offset); |
| final int? outerCodeUnit = _text.codeUnitAt(forward ? offset : offset - 1); |
| |
| // Make sure the hard break rules in UAX#29 take precedence over the ones we |
| // add below. Luckily there're only 4 hard break rules for word breaks, and |
| // dictionary based breaking does not introduce new hard breaks: |
| // https://unicode-org.github.io/icu/userguide/boundaryanalysis/break-rules.html#word-dictionaries |
| // |
| // WB1 & WB2: always break at the start or the end of the text. |
| final bool hardBreakRulesApply = innerCodePoint == null || outerCodeUnit == null |
| // WB3a & WB3b: always break before and after newlines. |
| || _isNewline(innerCodePoint) || _isNewline(outerCodeUnit); |
| return hardBreakRulesApply || !RegExp(r'[\p{Space_Separator}\p{Punctuation}]', unicode: true).hasMatch(String.fromCharCode(innerCodePoint)); |
| } |
| |
| /// Returns a [TextBoundary] suitable for handling keyboard navigation |
| /// commands that change the current selection word by word. |
| /// |
| /// This [TextBoundary] is used by text widgets in the flutter framework to |
| /// provide default implementation for text editing shortcuts, for example, |
| /// "delete to the previous word". |
| /// |
| /// The implementation applies the same set of rules [WordBoundary] uses, |
| /// except that word breaks end on a space separator or a punctuation will be |
| /// skipped, to match the behavior of most platforms. Additional rules may be |
| /// added in the future to better match platform behaviors. |
| late final TextBoundary moveByWordBoundary = _UntilTextBoundary(this, _skipSpacesAndPunctuations); |
| } |
| |
| class _UntilTextBoundary extends TextBoundary { |
| const _UntilTextBoundary(this._textBoundary, this._predicate); |
| |
| final UntilPredicate _predicate; |
| final TextBoundary _textBoundary; |
| |
| @override |
| int? getLeadingTextBoundaryAt(int position) { |
| if (position < 0) { |
| return null; |
| } |
| final int? offset = _textBoundary.getLeadingTextBoundaryAt(position); |
| return offset == null || _predicate(offset, false) |
| ? offset |
| : getLeadingTextBoundaryAt(offset - 1); |
| } |
| |
| @override |
| int? getTrailingTextBoundaryAt(int position) { |
| final int? offset = _textBoundary.getTrailingTextBoundaryAt(max(position, 0)); |
| return offset == null || _predicate(offset, true) |
| ? offset |
| : getTrailingTextBoundaryAt(offset); |
| } |
| } |
| |
| class _TextLayout { |
| _TextLayout._(this._paragraph, this.writingDirection, this.rawString); |
| |
| final TextDirection writingDirection; |
| final String rawString; |
| |
| // This field is not final because the owner TextPainter could create a new |
| // ui.Paragraph with the exact same text layout (for example, when only the |
| // color of the text is changed). |
| // |
| // The creator of this _TextLayout is also responsible for disposing this |
| // object when it's no longer needed. |
| ui.Paragraph _paragraph; |
| |
| /// Whether this layout has been invalidated and disposed. |
| /// |
| /// Only for use when asserts are enabled. |
| bool get debugDisposed => _paragraph.debugDisposed; |
| |
| /// The horizontal space required to paint this text. |
| /// |
| /// If a line ends with trailing spaces, the trailing spaces may extend |
| /// outside of the horizontal paint bounds defined by [width]. |
| double get width => _paragraph.width; |
| |
| /// The vertical space required to paint this text. |
| double get height => _paragraph.height; |
| |
| /// The width at which decreasing the width of the text would prevent it from |
| /// painting itself completely within its bounds. |
| double get minIntrinsicLineExtent => _paragraph.minIntrinsicWidth; |
| |
| /// The width at which increasing the width of the text no longer decreases the height. |
| /// |
| /// Includes trailing spaces if any. |
| double get maxIntrinsicLineExtent => _paragraph.maxIntrinsicWidth; |
| |
| /// The distance from the left edge of the leftmost glyph to the right edge of |
| /// the rightmost glyph in the paragraph. |
| double get longestLine => _paragraph.longestLine; |
| |
| /// Returns the distance from the top of the text to the first baseline of the |
| /// given type. |
| double getDistanceToBaseline(TextBaseline baseline) { |
| return switch (baseline) { |
| TextBaseline.alphabetic => _paragraph.alphabeticBaseline, |
| TextBaseline.ideographic => _paragraph.ideographicBaseline, |
| }; |
| } |
| |
| /// The line caret metrics representing the end of text location. |
| /// |
| /// This is usually used when the caret is placed at the end of the text |
| /// (text.length, downstream), unless maxLines is set to a non-null value, in |
| /// which case the caret is placed at the visual end of the last visible line. |
| /// |
| /// This should not be called when the paragraph is empty as the implementation |
| /// relies on line metrics. |
| /// |
| /// When the last bidi level run in the paragraph and the paragraph's bidi |
| /// levels have opposite parities (which implies opposite writing directions), |
| /// this makes sure the caret is placed at the same "end" of the line as if the |
| /// line ended with a line feed. |
| late final _LineCaretMetrics _endOfTextCaretMetrics = _computeEndOfTextCaretAnchorOffset(); |
| _LineCaretMetrics _computeEndOfTextCaretAnchorOffset() { |
| final int lastLineIndex = _paragraph.numberOfLines - 1; |
| assert(lastLineIndex >= 0); |
| final ui.LineMetrics lineMetrics = _paragraph.getLineMetricsAt(lastLineIndex)!; |
| // SkParagraph currently treats " " and "\t" as white spaces. Trailing white |
| // spaces don't contribute to the line width and thus require special handling |
| // when they're present. |
| // Luckily they have the same bidi embedding level as the paragraph as per |
| // https://unicode.org/reports/tr9/#L1, so we can anchor the caret to the |
| // last logical trailing space. |
| final bool hasTrailingSpaces = switch (rawString.codeUnitAt(rawString.length - 1)) { |
| 0x9 || // horizontal tab |
| 0x20 => true, // space |
| _ => false, |
| }; |
| |
| final double baseline = lineMetrics.baseline; |
| final double dx; |
| late final ui.GlyphInfo? lastGlyph = _paragraph.getGlyphInfoAt(rawString.length - 1); |
| // TODO(LongCatIsLooong): handle the case where maxLine is set to non-null |
| // and the last line ends with trailing whitespaces. |
| if (hasTrailingSpaces && lastGlyph != null) { |
| final Rect glyphBounds = lastGlyph.graphemeClusterLayoutBounds; |
| assert(!glyphBounds.isEmpty); |
| dx = switch (writingDirection) { |
| TextDirection.ltr => glyphBounds.right, |
| TextDirection.rtl => glyphBounds.left, |
| }; |
| } else { |
| dx = switch (writingDirection) { |
| TextDirection.ltr => lineMetrics.left + lineMetrics.width, |
| TextDirection.rtl => lineMetrics.left, |
| }; |
| } |
| return _LineCaretMetrics(offset: Offset(dx, baseline), writingDirection: writingDirection); |
| } |
| |
| double _contentWidthFor(double minWidth, double maxWidth, TextWidthBasis widthBasis) { |
| return switch (widthBasis) { |
| TextWidthBasis.longestLine => clampDouble(longestLine, minWidth, maxWidth), |
| TextWidthBasis.parent => clampDouble(maxIntrinsicLineExtent, minWidth, maxWidth), |
| }; |
| } |
| } |
| |
| // This class stores the current text layout and the corresponding |
| // paintOffset/contentWidth, as well as some cached text metrics values that |
| // depends on the current text layout, which will be invalidated as soon as the |
| // text layout is invalidated. |
| class _TextPainterLayoutCacheWithOffset { |
| _TextPainterLayoutCacheWithOffset(this.layout, this.textAlignment, this.layoutMaxWidth, this.contentWidth) |
| : assert(textAlignment >= 0.0 && textAlignment <= 1.0), |
| assert(!layoutMaxWidth.isNaN), |
| assert(!contentWidth.isNaN); |
| |
| final _TextLayout layout; |
| |
| // The input width used to lay out the paragraph. |
| final double layoutMaxWidth; |
| |
| // The content width the text painter should report in TextPainter.width. |
| // This is also used to compute `paintOffset`. |
| double contentWidth; |
| |
| // The effective text alignment in the TextPainter's canvas. The value is |
| // within the [0, 1] interval: 0 for left aligned and 1 for right aligned. |
| final double textAlignment; |
| |
| // The paintOffset of the `paragraph` in the TextPainter's canvas. |
| // |
| // It's coordinate values are guaranteed to not be NaN. |
| Offset get paintOffset { |
| if (textAlignment == 0) { |
| return Offset.zero; |
| } |
| if (!paragraph.width.isFinite) { |
| return const Offset(double.infinity, 0.0); |
| } |
| final double dx = textAlignment * (contentWidth - paragraph.width); |
| assert(!dx.isNaN); |
| return Offset(dx, 0); |
| } |
| |
| ui.Paragraph get paragraph => layout._paragraph; |
| |
| // Try to resize the contentWidth to fit the new input constraints, by just |
| // adjusting the paint offset (so no line-breaking changes needed). |
| // |
| // Returns false if the new constraints require the text layout library to |
| // re-compute the line breaks. |
| bool _resizeToFit(double minWidth, double maxWidth, TextWidthBasis widthBasis) { |
| assert(layout.maxIntrinsicLineExtent.isFinite); |
| assert(minWidth <= maxWidth); |
| // The assumption here is that if a Paragraph's width is already >= its |
| // maxIntrinsicWidth, further increasing the input width does not change its |
| // layout (but may change the paint offset if it's not left-aligned). This is |
| // true even for TextAlign.justify: when width >= maxIntrinsicWidth |
| // TextAlign.justify will behave exactly the same as TextAlign.start. |
| // |
| // An exception to this is when the text is not left-aligned, and the input |
| // width is double.infinity. Since the resulting Paragraph will have a width |
| // of double.infinity, and to make the text visible the paintOffset.dx is |
| // bound to be double.negativeInfinity, which invalidates all arithmetic |
| // operations. |
| |
| if (maxWidth == contentWidth && minWidth == contentWidth) { |
| contentWidth = layout._contentWidthFor(minWidth, maxWidth, widthBasis); |
| return true; |
| } |
| |
| // Special case: |
| // When the paint offset and the paragraph width are both +∞, it's likely |
| // that the text layout engine skipped layout because there weren't anything |
| // to paint. Always try to re-compute the text layout. |
| if (!paintOffset.dx.isFinite && !paragraph.width.isFinite && minWidth.isFinite) { |
| assert(paintOffset.dx == double.infinity); |
| assert(paragraph.width == double.infinity); |
| return false; |
| } |
| |
| final double maxIntrinsicWidth = paragraph.maxIntrinsicWidth; |
| // Skip line breaking if the input width remains the same, of there will be |
| // no soft breaks. |
| final bool skipLineBreaking = maxWidth == layoutMaxWidth // Same input max width so relayout is unnecessary. |
| || ((paragraph.width - maxIntrinsicWidth) > -precisionErrorTolerance && (maxWidth - maxIntrinsicWidth) > -precisionErrorTolerance); |
| if (skipLineBreaking) { |
| // Adjust the content width in case the TextWidthBasis changed. |
| contentWidth = layout._contentWidthFor(minWidth, maxWidth, widthBasis); |
| return true; |
| } |
| return false; |
| } |
| |
| // ---- Cached Values ---- |
| |
| List<TextBox> get inlinePlaceholderBoxes => _cachedInlinePlaceholderBoxes ??= paragraph.getBoxesForPlaceholders(); |
| List<TextBox>? _cachedInlinePlaceholderBoxes; |
| |
| List<ui.LineMetrics> get lineMetrics => _cachedLineMetrics ??= paragraph.computeLineMetrics(); |
| List<ui.LineMetrics>? _cachedLineMetrics; |
| |
| // Used to determine whether the caret metrics cache should be invalidated. |
| int? _previousCaretPositionKey; |
| } |
| |
| /// The _CaretMetrics for carets located in a non-empty paragraph. Such carets |
| /// are anchored to the trailing edge or the leading edge of a glyph, or a |
| /// ligature component. |
| final class _LineCaretMetrics { |
| const _LineCaretMetrics({required this.offset, required this.writingDirection}); |
| /// The offset from the top left corner of the paragraph to the caret's top |
| /// start location. |
| final Offset offset; |
| |
| /// The writing direction of the glyph the _LineCaretMetrics is associated with. |
| /// The value determines whether the cursor is painted to the left or to the |
| /// right of [offset]. |
| final TextDirection writingDirection; |
| |
| _LineCaretMetrics shift(Offset offset) { |
| return offset == Offset.zero |
| ? this |
| : _LineCaretMetrics(offset: offset + this.offset, writingDirection: writingDirection); |
| } |
| } |
| |
| const String _flutterPaintingLibrary = 'package:flutter/painting.dart'; |
| |
| /// An object that paints a [TextSpan] tree into a [Canvas]. |
| /// |
| /// To use a [TextPainter], follow these steps: |
| /// |
| /// 1. Create a [TextSpan] tree and pass it to the [TextPainter] |
| /// constructor. |
| /// |
| /// 2. Call [layout] to prepare the paragraph. |
| /// |
| /// 3. Call [paint] as often as desired to paint the paragraph. |
| /// |
| /// 4. Call [dispose] when the object will no longer be accessed to release |
| /// native resources. For [TextPainter] objects that are used repeatedly and |
| /// stored on a [State] or [RenderObject], call [dispose] from |
| /// [State.dispose] or [RenderObject.dispose] or similar. For [TextPainter] |
| /// objects that are only used ephemerally, it is safe to immediately dispose |
| /// them after the last call to methods or properties on the object. |
| /// |
| /// If the width of the area into which the text is being painted |
| /// changes, return to step 2. If the text to be painted changes, |
| /// return to step 1. |
| /// |
| /// The default text style is white. To change the color of the text, |
| /// pass a [TextStyle] object to the [TextSpan] in `text`. |
| class TextPainter { |
| /// Creates a text painter that paints the given text. |
| /// |
| /// The `text` and `textDirection` arguments are optional but [text] and |
| /// [textDirection] must be non-null before calling [layout]. |
| /// |
| /// The [maxLines] property, if non-null, must be greater than zero. |
| TextPainter({ |
| InlineSpan? text, |
| TextAlign textAlign = TextAlign.start, |
| TextDirection? textDirection, |
| @Deprecated( |
| 'Use textScaler instead. ' |
| 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' |
| 'This feature was deprecated after v3.12.0-2.0.pre.', |
| ) |
| double textScaleFactor = 1.0, |
| TextScaler textScaler = TextScaler.noScaling, |
| int? maxLines, |
| String? ellipsis, |
| Locale? locale, |
| StrutStyle? strutStyle, |
| TextWidthBasis textWidthBasis = TextWidthBasis.parent, |
| TextHeightBehavior? textHeightBehavior, |
| }) : assert(text == null || text.debugAssertIsValid()), |
| assert(maxLines == null || maxLines > 0), |
| assert(textScaleFactor == 1.0 || identical(textScaler, TextScaler.noScaling), 'Use textScaler instead.'), |
| _text = text, |
| _textAlign = textAlign, |
| _textDirection = textDirection, |
| _textScaler = textScaler == TextScaler.noScaling ? TextScaler.linear(textScaleFactor) : textScaler, |
| _maxLines = maxLines, |
| _ellipsis = ellipsis, |
| _locale = locale, |
| _strutStyle = strutStyle, |
| _textWidthBasis = textWidthBasis, |
| _textHeightBehavior = textHeightBehavior { |
| // TODO(polina-c): stop duplicating code across disposables |
| // https://github.com/flutter/flutter/issues/137435 |
| if (kFlutterMemoryAllocationsEnabled) { |
| FlutterMemoryAllocations.instance.dispatchObjectCreated( |
| library: _flutterPaintingLibrary, |
| className: '$TextPainter', |
| object: this, |
| ); |
| } |
| } |
| |
| /// Computes the width of a configured [TextPainter]. |
| /// |
| /// This is a convenience method that creates a text painter with the supplied |
| /// parameters, lays it out with the supplied [minWidth] and [maxWidth], and |
| /// returns its [TextPainter.width] making sure to dispose the underlying |
| /// resources. Doing this operation is expensive and should be avoided |
| /// whenever it is possible to preserve the [TextPainter] to paint the |
| /// text or get other information about it. |
| static double computeWidth({ |
| required InlineSpan text, |
| required TextDirection textDirection, |
| TextAlign textAlign = TextAlign.start, |
| @Deprecated( |
| 'Use textScaler instead. ' |
| 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' |
| 'This feature was deprecated after v3.12.0-2.0.pre.', |
| ) |
| double textScaleFactor = 1.0, |
| TextScaler textScaler = TextScaler.noScaling, |
| int? maxLines, |
| String? ellipsis, |
| Locale? locale, |
| StrutStyle? strutStyle, |
| TextWidthBasis textWidthBasis = TextWidthBasis.parent, |
| TextHeightBehavior? textHeightBehavior, |
| double minWidth = 0.0, |
| double maxWidth = double.infinity, |
| }) { |
| assert( |
| textScaleFactor == 1.0 || identical(textScaler, TextScaler.noScaling), |
| 'Use textScaler instead.', |
| ); |
| final TextPainter painter = TextPainter( |
| text: text, |
| textAlign: textAlign, |
| textDirection: textDirection, |
| textScaler: textScaler == TextScaler.noScaling ? TextScaler.linear(textScaleFactor) : textScaler, |
| maxLines: maxLines, |
| ellipsis: ellipsis, |
| locale: locale, |
| strutStyle: strutStyle, |
| textWidthBasis: textWidthBasis, |
| textHeightBehavior: textHeightBehavior, |
| )..layout(minWidth: minWidth, maxWidth: maxWidth); |
| |
| try { |
| return painter.width; |
| } finally { |
| painter.dispose(); |
| } |
| } |
| |
| /// Computes the max intrinsic width of a configured [TextPainter]. |
| /// |
| /// This is a convenience method that creates a text painter with the supplied |
| /// parameters, lays it out with the supplied [minWidth] and [maxWidth], and |
| /// returns its [TextPainter.maxIntrinsicWidth] making sure to dispose the |
| /// underlying resources. Doing this operation is expensive and should be avoided |
| /// whenever it is possible to preserve the [TextPainter] to paint the |
| /// text or get other information about it. |
| static double computeMaxIntrinsicWidth({ |
| required InlineSpan text, |
| required TextDirection textDirection, |
| TextAlign textAlign = TextAlign.start, |
| @Deprecated( |
| 'Use textScaler instead. ' |
| 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' |
| 'This feature was deprecated after v3.12.0-2.0.pre.', |
| ) |
| double textScaleFactor = 1.0, |
| TextScaler textScaler = TextScaler.noScaling, |
| int? maxLines, |
| String? ellipsis, |
| Locale? locale, |
| StrutStyle? strutStyle, |
| TextWidthBasis textWidthBasis = TextWidthBasis.parent, |
| TextHeightBehavior? textHeightBehavior, |
| double minWidth = 0.0, |
| double maxWidth = double.infinity, |
| }) { |
| assert( |
| textScaleFactor == 1.0 || identical(textScaler, TextScaler.noScaling), |
| 'Use textScaler instead.', |
| ); |
| final TextPainter painter = TextPainter( |
| text: text, |
| textAlign: textAlign, |
| textDirection: textDirection, |
| textScaler: textScaler == TextScaler.noScaling ? TextScaler.linear(textScaleFactor) : textScaler, |
| maxLines: maxLines, |
| ellipsis: ellipsis, |
| locale: locale, |
| strutStyle: strutStyle, |
| textWidthBasis: textWidthBasis, |
| textHeightBehavior: textHeightBehavior, |
| )..layout(minWidth: minWidth, maxWidth: maxWidth); |
| |
| try { |
| return painter.maxIntrinsicWidth; |
| } finally { |
| painter.dispose(); |
| } |
| } |
| |
| // Whether textWidthBasis has changed after the most recent `layout` call. |
| bool _debugNeedsRelayout = true; |
| // The result of the most recent `layout` call. |
| _TextPainterLayoutCacheWithOffset? _layoutCache; |
| |
| // Whether _layoutCache contains outdated paint information and needs to be |
| // updated before painting. |
| // |
| // ui.Paragraph is entirely immutable, thus text style changes that can affect |
| // layout and those who can't both require the ui.Paragraph object being |
| // recreated. The caller may not call `layout` again after text color is |
| // updated. See: https://github.com/flutter/flutter/issues/85108 |
| bool _rebuildParagraphForPaint = true; |
| |
| bool get _debugAssertTextLayoutIsValid { |
| assert(!debugDisposed); |
| if (_layoutCache == null) { |
| throw FlutterError.fromParts(<DiagnosticsNode>[ |
| ErrorSummary('Text layout not available'), |
| if (_debugMarkNeedsLayoutCallStack != null) DiagnosticsStackTrace('The calls that first invalidated the text layout were', _debugMarkNeedsLayoutCallStack) |
| else ErrorDescription('The TextPainter has never been laid out.') |
| ]); |
| } |
| return true; |
| } |
| |
| StackTrace? _debugMarkNeedsLayoutCallStack; |
| |
| /// Marks this text painter's layout information as dirty and removes cached |
| /// information. |
| /// |
| /// Uses this method to notify text painter to relayout in the case of |
| /// layout changes in engine. In most cases, updating text painter properties |
| /// in framework will automatically invoke this method. |
| void markNeedsLayout() { |
| assert(() { |
| if (_layoutCache != null) { |
| _debugMarkNeedsLayoutCallStack ??= StackTrace.current; |
| } |
| return true; |
| }()); |
| _layoutCache?.paragraph.dispose(); |
| _layoutCache = null; |
| } |
| |
| /// The (potentially styled) text to paint. |
| /// |
| /// After this is set, you must call [layout] before the next call to [paint]. |
| /// This and [textDirection] must be non-null before you call [layout]. |
| /// |
| /// The [InlineSpan] this provides is in the form of a tree that may contain |
| /// multiple instances of [TextSpan]s and [WidgetSpan]s. To obtain a plain text |
| /// representation of the contents of this [TextPainter], use [plainText]. |
| InlineSpan? get text => _text; |
| InlineSpan? _text; |
| set text(InlineSpan? value) { |
| assert(value == null || value.debugAssertIsValid()); |
| if (_text == value) { |
| return; |
| } |
| if (_text?.style != value?.style) { |
| _layoutTemplate?.dispose(); |
| _layoutTemplate = null; |
| } |
| |
| final RenderComparison comparison = value == null |
| ? RenderComparison.layout |
| : _text?.compareTo(value) ?? RenderComparison.layout; |
| |
| _text = value; |
| _cachedPlainText = null; |
| |
| if (comparison.index >= RenderComparison.layout.index) { |
| markNeedsLayout(); |
| } else if (comparison.index >= RenderComparison.paint.index) { |
| // Don't invalid the _layoutCache just yet. It still contains valid layout |
| // information. |
| _rebuildParagraphForPaint = true; |
| } |
| // Neither relayout or repaint is needed. |
| } |
| |
| /// Returns a plain text version of the text to paint. |
| /// |
| /// This uses [InlineSpan.toPlainText] to get the full contents of all nodes in the tree. |
| String get plainText { |
| _cachedPlainText ??= _text?.toPlainText(includeSemanticsLabels: false); |
| return _cachedPlainText ?? ''; |
| } |
| String? _cachedPlainText; |
| |
| /// How the text should be aligned horizontally. |
| /// |
| /// After this is set, you must call [layout] before the next call to [paint]. |
| /// |
| /// The [textAlign] property defaults to [TextAlign.start]. |
| TextAlign get textAlign => _textAlign; |
| TextAlign _textAlign; |
| set textAlign(TextAlign value) { |
| if (_textAlign == value) { |
| return; |
| } |
| _textAlign = value; |
| markNeedsLayout(); |
| } |
| |
| /// The default directionality of the text. |
| /// |
| /// This controls how the [TextAlign.start], [TextAlign.end], and |
| /// [TextAlign.justify] values of [textAlign] are resolved. |
| /// |
| /// This is also used to disambiguate how to render bidirectional text. For |
| /// example, if the [text] is an English phrase followed by a Hebrew phrase, |
| /// in a [TextDirection.ltr] context the English phrase will be on the left |
| /// and the Hebrew phrase to its right, while in a [TextDirection.rtl] |
| /// context, the English phrase will be on the right and the Hebrew phrase on |
| /// its left. |
| /// |
| /// After this is set, you must call [layout] before the next call to [paint]. |
| /// |
| /// This and [text] must be non-null before you call [layout]. |
| TextDirection? get textDirection => _textDirection; |
| TextDirection? _textDirection; |
| set textDirection(TextDirection? value) { |
| if (_textDirection == value) { |
| return; |
| } |
| _textDirection = value; |
| markNeedsLayout(); |
| _layoutTemplate?.dispose(); |
| _layoutTemplate = null; // Shouldn't really matter, but for strict correctness... |
| } |
| |
| /// Deprecated. Will be removed in a future version of Flutter. Use |
| /// [textScaler] instead. |
| /// |
| /// The number of font pixels for each logical pixel. |
| /// |
| /// For example, if the text scale factor is 1.5, text will be 50% larger than |
| /// the specified font size. |
| /// |
| /// After this is set, you must call [layout] before the next call to [paint]. |
| @Deprecated( |
| 'Use textScaler instead. ' |
| 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' |
| 'This feature was deprecated after v3.12.0-2.0.pre.', |
| ) |
| double get textScaleFactor => textScaler.textScaleFactor; |
| @Deprecated( |
| 'Use textScaler instead. ' |
| 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' |
| 'This feature was deprecated after v3.12.0-2.0.pre.', |
| ) |
| set textScaleFactor(double value) { |
| textScaler = TextScaler.linear(value); |
| } |
| |
| /// {@template flutter.painting.textPainter.textScaler} |
| /// The font scaling strategy to use when laying out and rendering the text. |
| /// |
| /// The value usually comes from [MediaQuery.textScalerOf], which typically |
| /// reflects the user-specified text scaling value in the platform's |
| /// accessibility settings. The [TextStyle.fontSize] of the text will be |
| /// adjusted by the [TextScaler] before the text is laid out and rendered. |
| /// {@endtemplate} |
| /// |
| /// The [layout] method must be called after [textScaler] changes as it |
| /// affects the text layout. |
| TextScaler get textScaler => _textScaler; |
| TextScaler _textScaler; |
| set textScaler(TextScaler value) { |
| if (value == _textScaler) { |
| return; |
| } |
| _textScaler = value; |
| markNeedsLayout(); |
| _layoutTemplate?.dispose(); |
| _layoutTemplate = null; |
| } |
| |
| /// The string used to ellipsize overflowing text. Setting this to a non-empty |
| /// string will cause this string to be substituted for the remaining text |
| /// if the text can not fit within the specified maximum width. |
| /// |
| /// Specifically, the ellipsis is applied to the last line before the line |
| /// truncated by [maxLines], if [maxLines] is non-null and that line overflows |
| /// the width constraint, or to the first line that is wider than the width |
| /// constraint, if [maxLines] is null. The width constraint is the `maxWidth` |
| /// passed to [layout]. |
| /// |
| /// After this is set, you must call [layout] before the next call to [paint]. |
| /// |
| /// The higher layers of the system, such as the [Text] widget, represent |
| /// overflow effects using the [TextOverflow] enum. The |
| /// [TextOverflow.ellipsis] value corresponds to setting this property to |
| /// U+2026 HORIZONTAL ELLIPSIS (…). |
| String? get ellipsis => _ellipsis; |
| String? _ellipsis; |
| set ellipsis(String? value) { |
| assert(value == null || value.isNotEmpty); |
| if (_ellipsis == value) { |
| return; |
| } |
| _ellipsis = value; |
| markNeedsLayout(); |
| } |
| |
| /// The locale used to select region-specific glyphs. |
| Locale? get locale => _locale; |
| Locale? _locale; |
| set locale(Locale? value) { |
| if (_locale == value) { |
| return; |
| } |
| _locale = value; |
| markNeedsLayout(); |
| } |
| |
| /// An optional maximum number of lines for the text to span, wrapping if |
| /// necessary. |
| /// |
| /// If the text exceeds the given number of lines, it is truncated such that |
| /// subsequent lines are dropped. |
| /// |
| /// After this is set, you must call [layout] before the next call to [paint]. |
| int? get maxLines => _maxLines; |
| int? _maxLines; |
| /// The value may be null. If it is not null, then it must be greater than zero. |
| set maxLines(int? value) { |
| assert(value == null || value > 0); |
| if (_maxLines == value) { |
| return; |
| } |
| _maxLines = value; |
| markNeedsLayout(); |
| } |
| |
| /// {@template flutter.painting.textPainter.strutStyle} |
| /// The strut style to use. Strut style defines the strut, which sets minimum |
| /// vertical layout metrics. |
| /// |
| /// Omitting or providing null will disable strut. |
| /// |
| /// Omitting or providing null for any properties of [StrutStyle] will result in |
| /// default values being used. It is highly recommended to at least specify a |
| /// [StrutStyle.fontSize]. |
| /// |
| /// See [StrutStyle] for details. |
| /// {@endtemplate} |
| StrutStyle? get strutStyle => _strutStyle; |
| StrutStyle? _strutStyle; |
| set strutStyle(StrutStyle? value) { |
| if (_strutStyle == value) { |
| return; |
| } |
| _strutStyle = value; |
| markNeedsLayout(); |
| } |
| |
| /// {@template flutter.painting.textPainter.textWidthBasis} |
| /// Defines how to measure the width of the rendered text. |
| /// {@endtemplate} |
| TextWidthBasis get textWidthBasis => _textWidthBasis; |
| TextWidthBasis _textWidthBasis; |
| set textWidthBasis(TextWidthBasis value) { |
| if (_textWidthBasis == value) { |
| return; |
| } |
| assert(() { return _debugNeedsRelayout = true; }()); |
| _textWidthBasis = value; |
| } |
| |
| /// {@macro dart.ui.textHeightBehavior} |
| TextHeightBehavior? get textHeightBehavior => _textHeightBehavior; |
| TextHeightBehavior? _textHeightBehavior; |
| set textHeightBehavior(TextHeightBehavior? value) { |
| if (_textHeightBehavior == value) { |
| return; |
| } |
| _textHeightBehavior = value; |
| markNeedsLayout(); |
| } |
| |
| /// An ordered list of [TextBox]es that bound the positions of the placeholders |
| /// in the paragraph. |
| /// |
| /// Each box corresponds to a [PlaceholderSpan] in the order they were defined |
| /// in the [InlineSpan] tree. |
| List<TextBox>? get inlinePlaceholderBoxes { |
| final _TextPainterLayoutCacheWithOffset? layout = _layoutCache; |
| if (layout == null) { |
| return null; |
| } |
| final Offset offset = layout.paintOffset; |
| if (!offset.dx.isFinite || !offset.dy.isFinite) { |
| return <TextBox>[]; |
| } |
| final List<TextBox> rawBoxes = layout.inlinePlaceholderBoxes; |
| if (offset == Offset.zero) { |
| return rawBoxes; |
| } |
| return rawBoxes.map((TextBox box) => _shiftTextBox(box, offset)).toList(growable: false); |
| } |
| |
| /// Sets the dimensions of each placeholder in [text]. |
| /// |
| /// The number of [PlaceholderDimensions] provided should be the same as the |
| /// number of [PlaceholderSpan]s in text. Passing in an empty or null `value` |
| /// will do nothing. |
| /// |
| /// If [layout] is attempted without setting the placeholder dimensions, the |
| /// placeholders will be ignored in the text layout and no valid |
| /// [inlinePlaceholderBoxes] will be returned. |
| void setPlaceholderDimensions(List<PlaceholderDimensions>? value) { |
| if (value == null || value.isEmpty || listEquals(value, _placeholderDimensions)) { |
| return; |
| } |
| assert(() { |
| int placeholderCount = 0; |
| text!.visitChildren((InlineSpan span) { |
| if (span is PlaceholderSpan) { |
| placeholderCount += 1; |
| } |
| return value.length >= placeholderCount; |
| }); |
| return placeholderCount == value.length; |
| }()); |
| _placeholderDimensions = value; |
| markNeedsLayout(); |
| } |
| List<PlaceholderDimensions>? _placeholderDimensions; |
| |
| ui.ParagraphStyle _createParagraphStyle([ TextAlign? textAlignOverride ]) { |
| assert(textDirection != null, 'TextPainter.textDirection must be set to a non-null value before using the TextPainter.'); |
| final TextStyle baseStyle = _text?.style ?? const TextStyle(); |
| return baseStyle.getParagraphStyle( |
| textAlign: textAlignOverride ?? textAlign, |
| textDirection: textDirection, |
| textScaler: textScaler, |
| maxLines: _maxLines, |
| textHeightBehavior: _textHeightBehavior, |
| ellipsis: _ellipsis, |
| locale: _locale, |
| strutStyle: _strutStyle, |
| ); |
| } |
| |
| ui.Paragraph? _layoutTemplate; |
| ui.Paragraph _createLayoutTemplate() { |
| final ui.ParagraphBuilder builder = ui.ParagraphBuilder( |
| _createParagraphStyle(TextAlign.left), |
| ); // direction doesn't matter, text is just a space |
| final ui.TextStyle? textStyle = text?.style?.getTextStyle(textScaler: textScaler); |
| if (textStyle != null) { |
| builder.pushStyle(textStyle); |
| } |
| builder.addText(' '); |
| return builder.build() |
| ..layout(const ui.ParagraphConstraints(width: double.infinity)); |
| } |
| |
| ui.Paragraph _getOrCreateLayoutTemplate() => _layoutTemplate ??= _createLayoutTemplate(); |
| |
| /// The height of a space in [text] in logical pixels. |
| /// |
| /// Not every line of text in [text] will have this height, but this height |
| /// is "typical" for text in [text] and useful for sizing other objects |
| /// relative a typical line of text. |
| /// |
| /// Obtaining this value does not require calling [layout]. |
| /// |
| /// The style of the [text] property is used to determine the font settings |
| /// that contribute to the [preferredLineHeight]. If [text] is null or if it |
| /// specifies no styles, the default [TextStyle] values are used (a 10 pixel |
| /// sans-serif font). |
| double get preferredLineHeight => _getOrCreateLayoutTemplate().height; |
| |
| /// The width at which decreasing the width of the text would prevent it from |
| /// painting itself completely within its bounds. |
| /// |
| /// Valid only after [layout] has been called. |
| double get minIntrinsicWidth { |
| assert(_debugAssertTextLayoutIsValid); |
| return _layoutCache!.layout.minIntrinsicLineExtent; |
| } |
| |
| /// The width at which increasing the width of the text no longer decreases the height. |
| /// |
| /// Valid only after [layout] has been called. |
| double get maxIntrinsicWidth { |
| assert(_debugAssertTextLayoutIsValid); |
| return _layoutCache!.layout.maxIntrinsicLineExtent; |
| } |
| |
| /// The horizontal space required to paint this text. |
| /// |
| /// Valid only after [layout] has been called. |
| double get width { |
| assert(_debugAssertTextLayoutIsValid); |
| assert(!_debugNeedsRelayout); |
| return _layoutCache!.contentWidth; |
| } |
| |
| /// The vertical space required to paint this text. |
| /// |
| /// Valid only after [layout] has been called. |
| double get height { |
| assert(_debugAssertTextLayoutIsValid); |
| return _layoutCache!.layout.height; |
| } |
| |
| /// The amount of space required to paint this text. |
| /// |
| /// Valid only after [layout] has been called. |
| Size get size { |
| assert(_debugAssertTextLayoutIsValid); |
| assert(!_debugNeedsRelayout); |
| return Size(width, height); |
| } |
| |
| /// Returns the distance from the top of the text to the first baseline of the |
| /// given type. |
| /// |
| /// Valid only after [layout] has been called. |
| double computeDistanceToActualBaseline(TextBaseline baseline) { |
| assert(_debugAssertTextLayoutIsValid); |
| return _layoutCache!.layout.getDistanceToBaseline(baseline); |
| } |
| |
| /// Whether any text was truncated or ellipsized. |
| /// |
| /// If [maxLines] is not null, this is true if there were more lines to be |
| /// drawn than the given [maxLines], and thus at least one line was omitted in |
| /// the output; otherwise it is false. |
| /// |
| /// If [maxLines] is null, this is true if [ellipsis] is not the empty string |
| /// and there was a line that overflowed the `maxWidth` argument passed to |
| /// [layout]; otherwise it is false. |
| /// |
| /// Valid only after [layout] has been called. |
| bool get didExceedMaxLines { |
| assert(_debugAssertTextLayoutIsValid); |
| return _layoutCache!.paragraph.didExceedMaxLines; |
| } |
| |
| // Creates a ui.Paragraph using the current configurations in this class and |
| // assign it to _paragraph. |
| ui.Paragraph _createParagraph(InlineSpan text) { |
| final ui.ParagraphBuilder builder = ui.ParagraphBuilder(_createParagraphStyle()); |
| text.build(builder, textScaler: textScaler, dimensions: _placeholderDimensions); |
| assert(() { |
| _debugMarkNeedsLayoutCallStack = null; |
| return true; |
| }()); |
| _rebuildParagraphForPaint = false; |
| return builder.build(); |
| } |
| |
| /// Computes the visual position of the glyphs for painting the text. |
| /// |
| /// The text will layout with a width that's as close to its max intrinsic |
| /// width (or its longest line, if [textWidthBasis] is set to |
| /// [TextWidthBasis.parent]) as possible while still being greater than or |
| /// equal to `minWidth` and less than or equal to `maxWidth`. |
| /// |
| /// The [text] and [textDirection] properties must be non-null before this is |
| /// called. |
| void layout({ double minWidth = 0.0, double maxWidth = double.infinity }) { |
| assert(!maxWidth.isNaN); |
| assert(!minWidth.isNaN); |
| assert(() { |
| _debugNeedsRelayout = false; |
| return true; |
| }()); |
| |
| final _TextPainterLayoutCacheWithOffset? cachedLayout = _layoutCache; |
| if (cachedLayout != null && cachedLayout._resizeToFit(minWidth, maxWidth, textWidthBasis)) { |
| return; |
| } |
| |
| final InlineSpan? text = this.text; |
| if (text == null) { |
| throw StateError('TextPainter.text must be set to a non-null value before using the TextPainter.'); |
| } |
| final TextDirection? textDirection = this.textDirection; |
| if (textDirection == null) { |
| throw StateError('TextPainter.textDirection must be set to a non-null value before using the TextPainter.'); |
| } |
| |
| final double paintOffsetAlignment = _computePaintOffsetFraction(textAlign, textDirection); |
| // Try to avoid laying out the paragraph with maxWidth=double.infinity |
| // when the text is not left-aligned, so we don't have to deal with an |
| // infinite paint offset. |
| final bool adjustMaxWidth = !maxWidth.isFinite && paintOffsetAlignment != 0; |
| final double? adjustedMaxWidth = !adjustMaxWidth ? maxWidth : cachedLayout?.layout.maxIntrinsicLineExtent; |
| final double layoutMaxWidth = adjustedMaxWidth ?? maxWidth; |
| |
| // Only rebuild the paragraph when there're layout changes, even when |
| // `_rebuildParagraphForPaint` is true. It's best to not eagerly rebuild |
| // the paragraph to avoid the extra work, because: |
| // 1. the text color could change again before `paint` is called (so one of |
| // the paragraph rebuilds is unnecessary) |
| // 2. the user could be measuring the text layout so `paint` will never be |
| // called. |
| final ui.Paragraph paragraph = (cachedLayout?.paragraph ?? _createParagraph(text)) |
| ..layout(ui.ParagraphConstraints(width: layoutMaxWidth)); |
| final _TextLayout layout = _TextLayout._(paragraph, textDirection, plainText); |
| final double contentWidth = layout._contentWidthFor(minWidth, maxWidth, textWidthBasis); |
| |
| final _TextPainterLayoutCacheWithOffset newLayoutCache; |
| // Call layout again if newLayoutCache had an infinite paint offset. |
| // This is not as expensive as it seems, line breaking is relatively cheap |
| // as compared to shaping. |
| if (adjustedMaxWidth == null && minWidth.isFinite) { |
| assert(maxWidth.isInfinite); |
| final double newInputWidth = layout.maxIntrinsicLineExtent; |
| paragraph.layout(ui.ParagraphConstraints(width: newInputWidth)); |
| newLayoutCache = _TextPainterLayoutCacheWithOffset(layout, paintOffsetAlignment, newInputWidth, contentWidth); |
| } else { |
| newLayoutCache = _TextPainterLayoutCacheWithOffset(layout, paintOffsetAlignment, layoutMaxWidth, contentWidth); |
| } |
| _layoutCache = newLayoutCache; |
| } |
| |
| /// Paints the text onto the given canvas at the given offset. |
| /// |
| /// Valid only after [layout] has been called. |
| /// |
| /// If you cannot see the text being painted, check that your text color does |
| /// not conflict with the background on which you are drawing. The default |
| /// text color is white (to contrast with the default black background color), |
| /// so if you are writing an application with a white background, the text |
| /// will not be visible by default. |
| /// |
| /// To set the text style, specify a [TextStyle] when creating the [TextSpan] |
| /// that you pass to the [TextPainter] constructor or to the [text] property. |
| void paint(Canvas canvas, Offset offset) { |
| final _TextPainterLayoutCacheWithOffset? layoutCache = _layoutCache; |
| if (layoutCache == null) { |
| throw StateError( |
| 'TextPainter.paint called when text geometry was not yet calculated.\n' |
| 'Please call layout() before paint() to position the text before painting it.', |
| ); |
| } |
| |
| if (!layoutCache.paintOffset.dx.isFinite || !layoutCache.paintOffset.dy.isFinite) { |
| return; |
| } |
| |
| if (_rebuildParagraphForPaint) { |
| Size? debugSize; |
| assert(() { |
| debugSize = size; |
| return true; |
| }()); |
| |
| final ui.Paragraph paragraph = layoutCache.paragraph; |
| // Unfortunately even if we know that there is only paint changes, there's |
| // no API to only make those updates so the paragraph has to be recreated |
| // and re-laid out. |
| assert(!layoutCache.layoutMaxWidth.isNaN); |
| layoutCache.layout._paragraph = _createParagraph(text!)..layout(ui.ParagraphConstraints(width: layoutCache.layoutMaxWidth)); |
| assert(paragraph.width == layoutCache.layout._paragraph.width); |
| paragraph.dispose(); |
| assert(debugSize == size); |
| } |
| assert(!_rebuildParagraphForPaint); |
| canvas.drawParagraph(layoutCache.paragraph, offset + layoutCache.paintOffset); |
| } |
| |
| // Returns true if value falls in the valid range of the UTF16 encoding. |
| static bool _isUTF16(int value) { |
| return value >= 0x0 && value <= 0xFFFFF; |
| } |
| |
| /// Returns true iff the given value is a valid UTF-16 high (first) surrogate. |
| /// The value must be a UTF-16 code unit, meaning it must be in the range |
| /// 0x0000-0xFFFF. |
| /// |
| /// See also: |
| /// * https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF |
| /// * [isLowSurrogate], which checks the same thing for low (second) |
| /// surrogates. |
| static bool isHighSurrogate(int value) { |
| assert(_isUTF16(value)); |
| return value & 0xFC00 == 0xD800; |
| } |
| |
| /// Returns true iff the given value is a valid UTF-16 low (second) surrogate. |
| /// The value must be a UTF-16 code unit, meaning it must be in the range |
| /// 0x0000-0xFFFF. |
| /// |
| /// See also: |
| /// * https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF |
| /// * [isHighSurrogate], which checks the same thing for high (first) |
| /// surrogates. |
| static bool isLowSurrogate(int value) { |
| assert(_isUTF16(value)); |
| return value & 0xFC00 == 0xDC00; |
| } |
| |
| /// Returns the closest offset after `offset` at which the input cursor can be |
| /// positioned. |
| int? getOffsetAfter(int offset) { |
| final int? nextCodeUnit = _text!.codeUnitAt(offset); |
| if (nextCodeUnit == null) { |
| return null; |
| } |
| // TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404). |
| return isHighSurrogate(nextCodeUnit) ? offset + 2 : offset + 1; |
| } |
| |
| /// Returns the closest offset before `offset` at which the input cursor can |
| /// be positioned. |
| int? getOffsetBefore(int offset) { |
| final int? prevCodeUnit = _text!.codeUnitAt(offset - 1); |
| if (prevCodeUnit == null) { |
| return null; |
| } |
| // TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404). |
| return isLowSurrogate(prevCodeUnit) ? offset - 2 : offset - 1; |
| } |
| |
| static double _computePaintOffsetFraction(TextAlign textAlign, TextDirection textDirection) { |
| return switch ((textAlign, textDirection)) { |
| (TextAlign.left, _) => 0.0, |
| (TextAlign.right, _) => 1.0, |
| (TextAlign.center, _) => 0.5, |
| (TextAlign.start || TextAlign.justify, TextDirection.ltr) => 0.0, |
| (TextAlign.start || TextAlign.justify, TextDirection.rtl) => 1.0, |
| (TextAlign.end, TextDirection.ltr) => 1.0, |
| (TextAlign.end, TextDirection.rtl) => 0.0, |
| }; |
| } |
| |
| /// Returns the offset at which to paint the caret. |
| /// |
| /// Valid only after [layout] has been called. |
| Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) { |
| final _TextPainterLayoutCacheWithOffset layoutCache = _layoutCache!; |
| final _LineCaretMetrics? caretMetrics = _computeCaretMetrics(position); |
| |
| if (caretMetrics == null) { |
| final double paintOffsetAlignment = _computePaintOffsetFraction(textAlign, textDirection!); |
| // The full width is not (width - caretPrototype.width), because |
| // RenderEditable reserves cursor width on the right. Ideally this |
| // should be handled by RenderEditable instead. |
| final double dx = paintOffsetAlignment == 0 ? 0 : paintOffsetAlignment * layoutCache.contentWidth; |
| return Offset(dx, 0.0); |
| } |
| |
| final Offset rawOffset = switch (caretMetrics) { |
| _LineCaretMetrics(writingDirection: TextDirection.ltr, :final Offset offset) => offset, |
| _LineCaretMetrics(writingDirection: TextDirection.rtl, :final Offset offset) => Offset(offset.dx - caretPrototype.width, offset.dy), |
| }; |
| // If offset.dx is outside of the advertised content area, then the associated |
| // glyph belongs to a trailing whitespace character. Ideally the behavior |
| // should be handled by higher-level implementations (for instance, |
| // RenderEditable reserves width for showing the caret, it's best to handle |
| // the clamping there). |
| final double adjustedDx = clampDouble(rawOffset.dx + layoutCache.paintOffset.dx, 0, layoutCache.contentWidth); |
| return Offset(adjustedDx, rawOffset.dy + layoutCache.paintOffset.dy); |
| } |
| |
| /// {@template flutter.painting.textPainter.getFullHeightForCaret} |
| /// Returns the strut bounded height of the glyph at the given `position`. |
| /// {@endtemplate} |
| /// |
| /// Valid only after [layout] has been called. |
| double? getFullHeightForCaret(TextPosition position, Rect caretPrototype) { |
| final TextBox textBox = _getOrCreateLayoutTemplate().getBoxesForRange(0, 1, boxHeightStyle: ui.BoxHeightStyle.strut).single; |
| return textBox.toRect().height; |
| } |
| |
| bool _isNewlineAtOffset(int offset) => 0 <= offset && offset < plainText.length |
| && WordBoundary._isNewline(plainText.codeUnitAt(offset)); |
| |
| // Cached caret metrics. This allows multiple invokes of [getOffsetForCaret] and |
| // [getFullHeightForCaret] in a row without performing redundant and expensive |
| // get rect calls to the paragraph. |
| // |
| // The cache implementation assumes there's only one cursor at any given time. |
| late _LineCaretMetrics _caretMetrics; |
| |
| // This function returns the caret's offset and height for the given |
| // `position` in the text, or null if the paragraph is empty. |
| // |
| // For a TextPosition, typically when its TextAffinity is downstream, the |
| // corresponding I-beam caret is anchored to the leading edge of the character |
| // at `offset` in the text. When the TextAffinity is upstream, the I-beam is |
| // then anchored to the trailing edge of the preceding character, except for a |
| // few edge cases: |
| // |
| // 1. empty paragraph: this method returns null and the caller handles this |
| // case. |
| // |
| // 2. (textLength, downstream), the end-of-text caret when the text is not |
| // empty: it's placed next to the trailing edge of the last line of the |
| // text, in case the text and its last bidi run have different writing |
| // directions. See the `_computeEndOfTextCaretAnchorOffset` method for more |
| // details. |
| // |
| // 3. (0, upstream), which isn't a valid position, but it's not a conventional |
| // "invalid" caret location either (the offset isn't negative). For |
| // historical reasons, this is treated as (0, downstream). |
| // |
| // 4. (x, upstream) where x - 1 points to a line break character. The caret |
| // should be displayed at the beginning of the newline instead of at the |
| // end of the previous line. Converts the location to (x, downstream). The |
| // choice we makes in 5. allows us to still check (x - 1) in case x points |
| // to a multi-code-unit character. |
| // |
| // 5. (x, downstream || upstream), where x points to a multi-code-unit |
| // character. There's no perfect caret placement in this case. Here we chose |
| // to draw the caret at the location that makes the most sense when the |
| // user wants to backspace (which also means it's left-arrow-key-biased): |
| // |
| // * downstream: show the caret at the leading edge of the character only if |
| // x points to the start of the grapheme. Otherwise show the caret at the |
| // leading edge of the next logical character. |
| // * upstream: show the caret at the trailing edge of the previous character |
| // only if x points to the start of the grapheme. Otherwise place the |
| // caret at the trailing edge of the character. |
| _LineCaretMetrics? _computeCaretMetrics(TextPosition position) { |
| assert(_debugAssertTextLayoutIsValid); |
| assert(!_debugNeedsRelayout); |
| |
| final _TextPainterLayoutCacheWithOffset cachedLayout = _layoutCache!; |
| // If nothing is laid out, top start is the only reasonable place to place |
| // the cursor. |
| // The HTML renderer reports numberOfLines == 1 when the text is empty: |
| // https://github.com/flutter/flutter/issues/143331 |
| if (cachedLayout.paragraph.numberOfLines < 1 || plainText.isEmpty) { |
| // TODO(LongCatIsLooong): assert when an invalid position is given. |
| return null; |
| } |
| |
| final (int offset, bool anchorToLeadingEdge) = switch (position) { |
| TextPosition(offset: 0) => (0, true), // As a special case, always anchor to the leading edge of the first grapheme regardless of the affinity. |
| TextPosition(:final int offset, affinity: TextAffinity.downstream) => (offset, true), |
| TextPosition(:final int offset, affinity: TextAffinity.upstream) when _isNewlineAtOffset(offset - 1) => (offset, true), |
| TextPosition(:final int offset, affinity: TextAffinity.upstream) => (offset - 1, false) |
| }; |
| |
| final int caretPositionCacheKey = anchorToLeadingEdge ? offset : -offset - 1; |
| if (caretPositionCacheKey == cachedLayout._previousCaretPositionKey) { |
| return _caretMetrics; |
| } |
| |
| final ui.GlyphInfo? glyphInfo = cachedLayout.paragraph.getGlyphInfoAt(offset); |
| |
| if (glyphInfo == null) { |
| // If the glyph isn't laid out, then the position points to a character |
| // that is not laid out. Use the EOT caret. |
| // TODO(LongCatIsLooong): assert when an invalid position is given. |
| final ui.Paragraph template = _getOrCreateLayoutTemplate(); |
| assert(template.numberOfLines == 1); |
| final double baselineOffset = template.getLineMetricsAt(0)!.baseline; |
| return cachedLayout.layout._endOfTextCaretMetrics.shift(Offset(0.0, -baselineOffset)); |
| } |
| |
| final TextRange graphemeRange = glyphInfo.graphemeClusterCodeUnitRange; |
| |
| // Works around a SkParagraph bug (https://github.com/flutter/flutter/issues/120836#issuecomment-1937343854): |
| // placeholders with a size of (0, 0) always have a rect of Rect.zero and a |
| // range of (0, 0). |
| if (graphemeRange.isCollapsed) { |
| assert(graphemeRange.start == 0); |
| return _computeCaretMetrics(TextPosition(offset: offset + 1)); |
| } |
| if (anchorToLeadingEdge && graphemeRange.start != offset) { |
| assert(graphemeRange.end > graphemeRange.start + 1); |
| // Addresses the case where `offset` points to a multi-code-unit grapheme |
| // that doesn't start at `offset`. |
| return _computeCaretMetrics(TextPosition(offset: graphemeRange.end)); |
| } |
| |
| final _LineCaretMetrics metrics; |
| final List<TextBox> boxes = cachedLayout.paragraph |
| .getBoxesForRange(graphemeRange.start, graphemeRange.end, boxHeightStyle: ui.BoxHeightStyle.strut); |
| if (boxes.isNotEmpty) { |
| final TextBox box = boxes.single; |
| metrics = _LineCaretMetrics( |
| offset: Offset(anchorToLeadingEdge ? box.start : box.end, box.top), |
| writingDirection: box.direction, |
| ); |
| } else { |
| // Fall back to glyphInfo. This should only happen when using the HTML renderer. |
| assert(kIsWeb && !isCanvasKit); |
| final Rect graphemeBounds = glyphInfo.graphemeClusterLayoutBounds; |
| final double dx = switch (glyphInfo.writingDirection) { |
| TextDirection.ltr => anchorToLeadingEdge ? graphemeBounds.left : graphemeBounds.right, |
| TextDirection.rtl => anchorToLeadingEdge ? graphemeBounds.right : graphemeBounds.left, |
| }; |
| metrics = _LineCaretMetrics( |
| offset: Offset(dx, graphemeBounds.top), |
| writingDirection: glyphInfo.writingDirection, |
| ); |
| } |
| |
| cachedLayout._previousCaretPositionKey = caretPositionCacheKey; |
| return _caretMetrics = metrics; |
| } |
| |
| /// Returns a list of rects that bound the given selection. |
| /// |
| /// The [selection] must be a valid range (with [TextSelection.isValid] true). |
| /// |
| /// The [boxHeightStyle] and [boxWidthStyle] arguments may be used to select |
| /// the shape of the [TextBox]s. These properties default to |
| /// [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively. |
| /// |
| /// A given selection might have more than one rect if this text painter |
| /// contains bidirectional text because logically contiguous text might not be |
| /// visually contiguous. |
| /// |
| /// Leading or trailing newline characters will be represented by zero-width |
| /// `TextBox`es. |
| /// |
| /// The method only returns `TextBox`es of glyphs that are entirely enclosed by |
| /// the given `selection`: a multi-code-unit glyph will be excluded if only |
| /// part of its code units are in `selection`. |
| List<TextBox> getBoxesForSelection( |
| TextSelection selection, { |
| ui.BoxHeightStyle boxHeightStyle = ui.BoxHeightStyle.tight, |
| ui.BoxWidthStyle boxWidthStyle = ui.BoxWidthStyle.tight, |
| }) { |
| assert(_debugAssertTextLayoutIsValid); |
| assert(selection.isValid); |
| assert(!_debugNeedsRelayout); |
| final _TextPainterLayoutCacheWithOffset cachedLayout = _layoutCache!; |
| final Offset offset = cachedLayout.paintOffset; |
| if (!offset.dx.isFinite || !offset.dy.isFinite) { |
| return <TextBox>[]; |
| } |
| final List<TextBox> boxes = cachedLayout.paragraph.getBoxesForRange( |
| selection.start, |
| selection.end, |
| boxHeightStyle: boxHeightStyle, |
| boxWidthStyle: boxWidthStyle, |
| ); |
| return offset == Offset.zero |
| ? boxes |
| : boxes.map((TextBox box) => _shiftTextBox(box, offset)).toList(growable: false); |
| } |
| |
| /// Returns the [GlyphInfo] of the glyph closest to the given `offset` in the |
| /// paragraph coordinate system, or null if the text is empty, or is entirely |
| /// clipped or ellipsized away. |
| /// |
| /// This method first finds the line closest to `offset.dy`, and then returns |
| /// the [GlyphInfo] of the closest glyph(s) within that line. |
| ui.GlyphInfo? getClosestGlyphForOffset(Offset offset) { |
| assert(_debugAssertTextLayoutIsValid); |
| assert(!_debugNeedsRelayout); |
| final _TextPainterLayoutCacheWithOffset cachedLayout = _layoutCache!; |
| final ui.GlyphInfo? rawGlyphInfo = cachedLayout.paragraph.getClosestGlyphInfoForOffset(offset - cachedLayout.paintOffset); |
| if (rawGlyphInfo == null || cachedLayout.paintOffset == Offset.zero) { |
| return rawGlyphInfo; |
| } |
| return ui.GlyphInfo(rawGlyphInfo.graphemeClusterLayoutBounds.shift(cachedLayout.paintOffset), rawGlyphInfo.graphemeClusterCodeUnitRange, rawGlyphInfo.writingDirection); |
| } |
| |
| /// Returns the closest position within the text for the given pixel offset. |
| TextPosition getPositionForOffset(Offset offset) { |
| assert(_debugAssertTextLayoutIsValid); |
| assert(!_debugNeedsRelayout); |
| final _TextPainterLayoutCacheWithOffset cachedLayout = _layoutCache!; |
| return cachedLayout.paragraph.getPositionForOffset(offset - cachedLayout.paintOffset); |
| } |
| |
| /// {@template flutter.painting.TextPainter.getWordBoundary} |
| /// Returns the text range of the word at the given offset. Characters not |
| /// part of a word, such as spaces, symbols, and punctuation, have word breaks |
| /// on both sides. In such cases, this method will return a text range that |
| /// contains the given text position. |
| /// |
| /// Word boundaries are defined more precisely in Unicode Standard Annex #29 |
| /// <http://www.unicode.org/reports/tr29/#Word_Boundaries>. |
| /// {@endtemplate} |
| TextRange getWordBoundary(TextPosition position) { |
| assert(_debugAssertTextLayoutIsValid); |
| return _layoutCache!.paragraph.getWordBoundary(position); |
| } |
| |
| /// {@template flutter.painting.TextPainter.wordBoundaries} |
| /// Returns a [TextBoundary] that can be used to perform word boundary analysis |
| /// on the current [text]. |
| /// |
| /// This [TextBoundary] uses word boundary rules defined in [Unicode Standard |
| /// Annex #29](http://www.unicode.org/reports/tr29/#Word_Boundaries). |
| /// {@endtemplate} |
| /// |
| /// Currently word boundary analysis can only be performed after [layout] |
| /// has been called. |
| WordBoundary get wordBoundaries => WordBoundary._(text!, _layoutCache!.paragraph); |
| |
| /// Returns the text range of the line at the given offset. |
| /// |
| /// The newline (if any) is not returned as part of the range. |
| TextRange getLineBoundary(TextPosition position) { |
| assert(_debugAssertTextLayoutIsValid); |
| return _layoutCache!.paragraph.getLineBoundary(position); |
| } |
| |
| static ui.LineMetrics _shiftLineMetrics(ui.LineMetrics metrics, Offset offset) { |
| assert(offset.dx.isFinite); |
| assert(offset.dy.isFinite); |
| return ui.LineMetrics( |
| hardBreak: metrics.hardBreak, |
| ascent: metrics.ascent, |
| descent: metrics.descent, |
| unscaledAscent: metrics.unscaledAscent, |
| height: metrics.height, |
| width: metrics.width, |
| left: metrics.left + offset.dx, |
| baseline: metrics.baseline + offset.dy, |
| lineNumber: metrics.lineNumber, |
| ); |
| } |
| |
| static TextBox _shiftTextBox(TextBox box, Offset offset) { |
| assert(offset.dx.isFinite); |
| assert(offset.dy.isFinite); |
| return TextBox.fromLTRBD( |
| box.left + offset.dx, |
| box.top + offset.dy, |
| box.right + offset.dx, |
| box.bottom + offset.dy, |
| box.direction, |
| ); |
| } |
| |
| /// Returns the full list of [LineMetrics] that describe in detail the various |
| /// metrics of each laid out line. |
| /// |
| /// The [LineMetrics] list is presented in the order of the lines they represent. |
| /// For example, the first line is in the zeroth index. |
| /// |
| /// [LineMetrics] contains measurements such as ascent, descent, baseline, and |
| /// width for the line as a whole, and may be useful for aligning additional |
| /// widgets to a particular line. |
| /// |
| /// Valid only after [layout] has been called. |
| List<ui.LineMetrics> computeLineMetrics() { |
| assert(_debugAssertTextLayoutIsValid); |
| assert(!_debugNeedsRelayout); |
| final _TextPainterLayoutCacheWithOffset layout = _layoutCache!; |
| final Offset offset = layout.paintOffset; |
| if (!offset.dx.isFinite || !offset.dy.isFinite) { |
| return const <ui.LineMetrics>[]; |
| } |
| final List<ui.LineMetrics> rawMetrics = layout.lineMetrics; |
| return offset == Offset.zero |
| ? rawMetrics |
| : rawMetrics.map((ui.LineMetrics metrics) => _shiftLineMetrics(metrics, offset)).toList(growable: false); |
| } |
| |
| bool _disposed = false; |
| |
| /// Whether this object has been disposed or not. |
| /// |
| /// Only for use when asserts are enabled. |
| bool get debugDisposed { |
| bool? disposed; |
| assert(() { |
| disposed = _disposed; |
| return true; |
| }()); |
| return disposed ?? (throw StateError('debugDisposed only available when asserts are on.')); |
| } |
| |
| /// Releases the resources associated with this painter. |
| /// |
| /// After disposal this painter is unusable. |
| void dispose() { |
| assert(() { |
| _disposed = true; |
| return true; |
| }()); |
| // TODO(polina-c): stop duplicating code across disposables |
| // https://github.com/flutter/flutter/issues/137435 |
| if (kFlutterMemoryAllocationsEnabled) { |
| FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this); |
| } |
| _layoutTemplate?.dispose(); |
| _layoutTemplate = null; |
| _layoutCache?.paragraph.dispose(); |
| _layoutCache = null; |
| _text = null; |
| } |
| } |