| // 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, min; |
| import 'dart:ui' as ui show |
| BoxHeightStyle, |
| BoxWidthStyle, |
| LineMetrics, |
| Paragraph, |
| ParagraphBuilder, |
| ParagraphConstraints, |
| ParagraphStyle, |
| PlaceholderAlignment, |
| TextHeightBehavior, |
| 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'; |
| |
| export 'package:flutter/services.dart' show TextRange, TextSelection; |
| |
| // The default font size if none is specified. This should be kept in |
| // sync with the default values in text_style.dart, as well as the |
| // defaults set in the engine (eg, 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]. |
| /// |
| /// The [size] and [alignment] properties are required and cannot be null. |
| /// |
| /// 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) { |
| return switch (codePoint) { |
| 0x000A || 0x0085 || 0x000B || 0x000C || 0x2028 || 0x2029 => true, |
| _ => 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 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 logner needed. |
| ui.Paragraph _paragraph; |
| |
| // TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/31707 |
| // remove this hack as well as the flooring in `layout`. |
| @pragma('vm:prefer-inline') |
| static double _applyFloatingPointHack(double layoutValue) => ui.ParagraphBuilder.shouldDisableRoundingHack ? layoutValue : layoutValue.ceilToDouble(); |
| |
| /// 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 => _applyFloatingPointHack(_paragraph.width); |
| |
| /// The vertical space required to paint this text. |
| double get height => _applyFloatingPointHack(_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 => _applyFloatingPointHack(_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 => _applyFloatingPointHack(_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 => _applyFloatingPointHack(_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, |
| }; |
| } |
| } |
| |
| // 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, double minWidth, double maxWidth, TextWidthBasis widthBasis) |
| : contentWidth = _contentWidthFor(minWidth, maxWidth, widthBasis, layout), |
| assert(textAlignment >= 0.0 && textAlignment <= 1.0); |
| |
| final _TextLayout layout; |
| |
| // 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; |
| |
| static double _contentWidthFor(double minWidth, double maxWidth, TextWidthBasis widthBasis, _TextLayout layout) { |
| // TODO(LongCatIsLooong): remove the rounding when _applyFloatingPointHack |
| // is removed. |
| if (!ui.ParagraphBuilder.shouldDisableRoundingHack) { |
| minWidth = minWidth.floorToDouble(); |
| maxWidth = maxWidth.floorToDouble(); |
| } |
| return switch (widthBasis) { |
| TextWidthBasis.longestLine => clampDouble(layout.longestLine, minWidth, maxWidth), |
| TextWidthBasis.parent => clampDouble(layout.maxIntrinsicLineExtent, minWidth, maxWidth), |
| }; |
| } |
| |
| // 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 re-computing the line breaks, |
| // in which case no side effects will occur. |
| bool _resizeToFit(double minWidth, double maxWidth, TextWidthBasis widthBasis) { |
| assert(layout.maxIntrinsicLineExtent.isFinite); |
| // 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. |
| final double newContentWidth = _contentWidthFor(minWidth, maxWidth, widthBasis, layout); |
| if (newContentWidth == contentWidth) { |
| return true; |
| } |
| assert(minWidth <= maxWidth); |
| // Always needsLayout when the current paintOffset and the paragraph width are not finite. |
| 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; |
| if ((paragraph.width - maxIntrinsicWidth) > -precisionErrorTolerance && (maxWidth - maxIntrinsicWidth) > -precisionErrorTolerance) { |
| // Adjust the paintOffset and contentWidth to the new input constraints. |
| contentWidth = newContentWidth; |
| 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; |
| |
| // Holds the TextPosition the last caret metrics were computed with. When new |
| // values are passed in, we recompute the caret metrics only as necessary. |
| TextPosition? _previousCaretPosition; |
| } |
| |
| /// This is used to cache and pass the computed metrics regarding the |
| /// caret's size and position. This is preferred due to the expensive |
| /// nature of the calculation. |
| /// |
| // A _CaretMetrics is either a _LineCaretMetrics or an _EmptyLineCaretMetrics. |
| @immutable |
| sealed class _CaretMetrics { } |
| |
| /// The _CaretMetrics for carets located in a non-empty line. Carets located in a |
| /// non-empty line are associated with a glyph within the same line. |
| final class _LineCaretMetrics implements _CaretMetrics { |
| const _LineCaretMetrics({required this.offset, required this.writingDirection, required this.fullHeight}); |
| /// The offset of the top left corner of the caret from the top left |
| /// corner of the paragraph. |
| final Offset offset; |
| /// The writing direction of the glyph the _CaretMetrics is associated with. |
| final TextDirection writingDirection; |
| /// The full height of the glyph at the caret position. |
| final double fullHeight; |
| } |
| |
| /// The _CaretMetrics for carets located in an empty line (when the text is |
| /// empty, or the caret is between two a newline characters). |
| final class _EmptyLineCaretMetrics implements _CaretMetrics { |
| const _EmptyLineCaretMetrics({ required this.lineVerticalOffset }); |
| |
| /// The y offset of the unoccupied line. |
| final double lineVerticalOffset; |
| } |
| |
| /// 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 [textAlign] property must not be null. |
| /// |
| /// 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, |
| ui.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; |
| |
| /// 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, |
| ui.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, |
| ui.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; |
| // `_layoutCache`'s input width. This is only needed because there's no API to |
| // create paint only updates that don't affect the text layout (e.g., changing |
| // the color of the text), on ui.Paragraph or ui.ParagraphBuilder. |
| double _inputWidth = double.nan; |
| |
| 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 must not be null. It 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} |
| ui.TextHeightBehavior? get textHeightBehavior => _textHeightBehavior; |
| ui.TextHeightBehavior? _textHeightBehavior; |
| set textHeightBehavior(ui.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([ TextDirection? defaultTextDirection ]) { |
| // The defaultTextDirection argument is used for preferredLineHeight in case |
| // textDirection hasn't yet been set. |
| assert(textDirection != null || defaultTextDirection != null, 'TextPainter.textDirection must be set to a non-null value before using the TextPainter.'); |
| return _text!.style?.getParagraphStyle( |
| textAlign: textAlign, |
| textDirection: textDirection ?? defaultTextDirection, |
| textScaler: textScaler, |
| maxLines: _maxLines, |
| textHeightBehavior: _textHeightBehavior, |
| ellipsis: _ellipsis, |
| locale: _locale, |
| strutStyle: _strutStyle, |
| ) ?? ui.ParagraphStyle( |
| textAlign: textAlign, |
| textDirection: textDirection ?? defaultTextDirection, |
| // Use the default font size to multiply by as RichText does not |
| // perform inheriting [TextStyle]s and would otherwise |
| // fail to apply textScaler. |
| fontSize: textScaler.scale(_kDefaultFontSize), |
| maxLines: maxLines, |
| textHeightBehavior: _textHeightBehavior, |
| ellipsis: ellipsis, |
| locale: locale, |
| ); |
| } |
| |
| ui.Paragraph? _layoutTemplate; |
| ui.Paragraph _createLayoutTemplate() { |
| final ui.ParagraphBuilder builder = ui.ParagraphBuilder( |
| _createParagraphStyle(TextDirection.rtl), |
| ); // 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)); |
| } |
| |
| /// 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 => (_layoutTemplate ??= _createLayoutTemplate()).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; |
| _inputWidth = 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: _inputWidth)); |
| final _TextPainterLayoutCacheWithOffset newLayoutCache = _TextPainterLayoutCacheWithOffset( |
| _TextLayout._(paragraph), paintOffsetAlignment, minWidth, maxWidth, textWidthBasis, |
| ); |
| // 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 = newLayoutCache.layout.maxIntrinsicLineExtent; |
| paragraph.layout(ui.ParagraphConstraints(width: newInputWidth)); |
| _inputWidth = newInputWidth; |
| } |
| _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(!_inputWidth.isNaN); |
| layoutCache.layout._paragraph = _createParagraph(text!)..layout(ui.ParagraphConstraints(width: _inputWidth)); |
| 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; |
| } |
| |
| // Checks if the glyph is either [Unicode.RLM] or [Unicode.LRM]. These values take |
| // up zero space and do not have valid bounding boxes around them. |
| // |
| // We do not directly use the [Unicode] constants since they are strings. |
| static bool _isUnicodeDirectionality(int value) { |
| return value == 0x200F || value == 0x200E; |
| } |
| |
| /// 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; |
| } |
| |
| // Unicode value for a zero width joiner character. |
| static const int _zwjUtf16 = 0x200d; |
| |
| // Get the caret metrics (in logical pixels) based off the near edge of the |
| // character upstream from the given string offset. |
| _CaretMetrics? _getMetricsFromUpstream(int offset) { |
| assert(offset >= 0); |
| final int plainTextLength = plainText.length; |
| if (plainTextLength == 0 || offset > plainTextLength) { |
| return null; |
| } |
| final int prevCodeUnit = plainText.codeUnitAt(max(0, offset - 1)); |
| |
| // If the upstream character is a newline, cursor is at start of next line |
| const int NEWLINE_CODE_UNIT = 10; |
| |
| // Check for multi-code-unit glyphs such as emojis or zero width joiner. |
| final bool needsSearch = isHighSurrogate(prevCodeUnit) || isLowSurrogate(prevCodeUnit) || _text!.codeUnitAt(offset) == _zwjUtf16 || _isUnicodeDirectionality(prevCodeUnit); |
| int graphemeClusterLength = needsSearch ? 2 : 1; |
| List<TextBox> boxes = <TextBox>[]; |
| while (boxes.isEmpty) { |
| final int prevRuneOffset = offset - graphemeClusterLength; |
| // Use BoxHeightStyle.strut to ensure that the caret's height fits within |
| // the line's height and is consistent throughout the line. |
| boxes = _layoutCache!.paragraph.getBoxesForRange(max(0, prevRuneOffset), offset, boxHeightStyle: ui.BoxHeightStyle.strut); |
| // When the range does not include a full cluster, no boxes will be returned. |
| if (boxes.isEmpty) { |
| // When we are at the beginning of the line, a non-surrogate position will |
| // return empty boxes. We break and try from downstream instead. |
| if (!needsSearch && prevCodeUnit == NEWLINE_CODE_UNIT) { |
| break; // Only perform one iteration if no search is required. |
| } |
| if (prevRuneOffset < -plainTextLength) { |
| break; // Stop iterating when beyond the max length of the text. |
| } |
| // Multiply by two to log(n) time cover the entire text span. This allows |
| // faster discovery of very long clusters and reduces the possibility |
| // of certain large clusters taking much longer than others, which can |
| // cause jank. |
| graphemeClusterLength *= 2; |
| continue; |
| } |
| |
| // Try to identify the box nearest the offset. This logic works when |
| // there's just one box, and when all boxes have the same direction. |
| // It may not work in bidi text: https://github.com/flutter/flutter/issues/123424 |
| final TextBox box = boxes.last.direction == TextDirection.ltr |
| ? boxes.last : boxes.first; |
| return prevCodeUnit == NEWLINE_CODE_UNIT |
| ? _EmptyLineCaretMetrics(lineVerticalOffset: box.bottom) |
| : _LineCaretMetrics(offset: Offset(box.end, box.top), writingDirection: box.direction, fullHeight: box.bottom - box.top); |
| } |
| return null; |
| } |
| |
| // Get the caret metrics (in logical pixels) based off the near edge of the |
| // character downstream from the given string offset. |
| _CaretMetrics? _getMetricsFromDownstream(int offset) { |
| assert(offset >= 0); |
| final int plainTextLength = plainText.length; |
| if (plainTextLength == 0) { |
| return null; |
| } |
| // We cap the offset at the final index of plain text. |
| final int nextCodeUnit = plainText.codeUnitAt(min(offset, plainTextLength - 1)); |
| |
| // Check for multi-code-unit glyphs such as emojis or zero width joiner |
| final bool needsSearch = isHighSurrogate(nextCodeUnit) || isLowSurrogate(nextCodeUnit) || nextCodeUnit == _zwjUtf16 || _isUnicodeDirectionality(nextCodeUnit); |
| int graphemeClusterLength = needsSearch ? 2 : 1; |
| List<TextBox> boxes = <TextBox>[]; |
| while (boxes.isEmpty) { |
| final int nextRuneOffset = offset + graphemeClusterLength; |
| // Use BoxHeightStyle.strut to ensure that the caret's height fits within |
| // the line's height and is consistent throughout the line. |
| boxes = _layoutCache!.paragraph.getBoxesForRange(offset, nextRuneOffset, boxHeightStyle: ui.BoxHeightStyle.strut); |
| // When the range does not include a full cluster, no boxes will be returned. |
| if (boxes.isEmpty) { |
| // When we are at the end of the line, a non-surrogate position will |
| // return empty boxes. We break and try from upstream instead. |
| if (!needsSearch) { |
| break; // Only perform one iteration if no search is required. |
| } |
| if (nextRuneOffset >= plainTextLength << 1) { |
| break; // Stop iterating when beyond the max length of the text. |
| } |
| // Multiply by two to log(n) time cover the entire text span. This allows |
| // faster discovery of very long clusters and reduces the possibility |
| // of certain large clusters taking much longer than others, which can |
| // cause jank. |
| graphemeClusterLength *= 2; |
| continue; |
| } |
| |
| // Try to identify the box nearest the offset. This logic works when |
| // there's just one box, and when all boxes have the same direction. |
| // It may not work in bidi text: https://github.com/flutter/flutter/issues/123424 |
| final TextBox box = boxes.first.direction == TextDirection.ltr |
| ? boxes.first : boxes.last; |
| return _LineCaretMetrics(offset: Offset(box.start, box.top), writingDirection: box.direction, fullHeight: box.bottom - box.top); |
| } |
| return null; |
| } |
| |
| 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, TextDirection.ltr) => 0.0, |
| (TextAlign.start, TextDirection.rtl) => 1.0, |
| (TextAlign.justify, TextDirection.ltr) => 0.0, |
| (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 _CaretMetrics caretMetrics; |
| final _TextPainterLayoutCacheWithOffset layoutCache = _layoutCache!; |
| if (position.offset < 0) { |
| // TODO(LongCatIsLooong): make this case impossible; see https://github.com/flutter/flutter/issues/79495 |
| caretMetrics = const _EmptyLineCaretMetrics(lineVerticalOffset: 0); |
| } else { |
| caretMetrics = _computeCaretMetrics(position); |
| } |
| |
| final Offset rawOffset; |
| switch (caretMetrics) { |
| case _EmptyLineCaretMetrics(:final double lineVerticalOffset): |
| 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, lineVerticalOffset); |
| case _LineCaretMetrics(writingDirection: TextDirection.ltr, :final Offset offset): |
| rawOffset = offset; |
| case _LineCaretMetrics(writingDirection: TextDirection.rtl, :final Offset offset): |
| rawOffset = Offset(offset.dx - caretPrototype.width, offset.dy); |
| } |
| // If offset.dx is outside of the advertised content area, then the associated |
| // glyph cluster belongs to a trailing newline 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) { |
| if (position.offset < 0) { |
| // TODO(LongCatIsLooong): make this case impossible; see https://github.com/flutter/flutter/issues/79495 |
| return null; |
| } |
| return switch (_computeCaretMetrics(position)) { |
| _LineCaretMetrics(:final double fullHeight) => fullHeight, |
| _EmptyLineCaretMetrics() => null, |
| }; |
| } |
| |
| // 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. |
| late _CaretMetrics _caretMetrics; |
| |
| // Checks if the [position] and [caretPrototype] have changed from the cached |
| // version and recomputes the metrics required to position the caret. |
| _CaretMetrics _computeCaretMetrics(TextPosition position) { |
| assert(_debugAssertTextLayoutIsValid); |
| assert(!_debugNeedsRelayout); |
| final _TextPainterLayoutCacheWithOffset cachedLayout = _layoutCache!; |
| if (position == cachedLayout._previousCaretPosition) { |
| return _caretMetrics; |
| } |
| final int offset = position.offset; |
| final _CaretMetrics? metrics = switch (position.affinity) { |
| TextAffinity.upstream => _getMetricsFromUpstream(offset) ?? _getMetricsFromDownstream(offset), |
| TextAffinity.downstream => _getMetricsFromDownstream(offset) ?? _getMetricsFromUpstream(offset), |
| }; |
| // Cache the input parameters to prevent repeat work later. |
| cachedLayout._previousCaretPosition = position; |
| return _caretMetrics = metrics ?? const _EmptyLineCaretMetrics(lineVerticalOffset: 0); |
| } |
| |
| /// 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 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; |
| }()); |
| _layoutTemplate?.dispose(); |
| _layoutTemplate = null; |
| _layoutCache?.paragraph.dispose(); |
| _layoutCache = null; |
| _text = null; |
| } |
| } |