Use a separate TextPainter for intrinsics calculation in RenderEditable and RenderParagraph (#144577)

Use a dedicated `TextPainter` for intrinsic size calculation in `RenderEditable` and `RenderParagraph`.

This is an implementation detail so the change should be covered by existing tests.  Performance wise this shouldn't be significantly slower since SkParagraph [caches the result of slower operations across different paragraphs](9c62e7b382/modules/skparagraph/src/ParagraphCache.cpp (L254-L272)). Existing benchmarks should be able to catch potential regressions (??).

The reason for making this change is to make sure that intrinsic size computations don't destroy text layout artifacts, so I can expose the text layout as a stream of immutable `TextLayout` objects, to signify other render objects that text-layout-dependent-cache (such as caches for `getBoxesForRange` which can be relatively slow to compute) should be invalidated and  `markNeedsPaint` needs to be called if the painting logic depended on text layout.
Without this change, the intrinsics/dry layout calculations will add additional events to the text layout stream, which violates the "dry"/non-destructive contract.
This commit is contained in:
LongCatIsLooong 2024-03-15 18:15:19 -07:00 committed by GitHub
parent 15e90ad224
commit cc01701781
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 350 additions and 166 deletions

View file

@ -410,6 +410,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
_selectionPainter.dispose(); _selectionPainter.dispose();
_caretPainter.dispose(); _caretPainter.dispose();
_textPainter.dispose(); _textPainter.dispose();
_textIntrinsicsCache?.dispose();
if (_disposeShowCursor) { if (_disposeShowCursor) {
_showCursor.dispose(); _showCursor.dispose();
_disposeShowCursor = false; _disposeShowCursor = false;
@ -510,18 +511,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
); );
} }
double? _textLayoutLastMaxWidth;
double? _textLayoutLastMinWidth;
/// Assert that the last layout still matches the constraints.
void debugAssertLayoutUpToDate() {
assert(
_textLayoutLastMaxWidth == constraints.maxWidth &&
_textLayoutLastMinWidth == constraints.minWidth,
'Last width ($_textLayoutLastMinWidth, $_textLayoutLastMaxWidth) not the same as max width constraint (${constraints.minWidth}, ${constraints.maxWidth}).',
);
}
/// Whether the [handleEvent] will propagate pointer events to selection /// Whether the [handleEvent] will propagate pointer events to selection
/// handlers. /// handlers.
/// ///
@ -542,7 +531,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return; return;
} }
_textPainter.textHeightBehavior = value; _textPainter.textHeightBehavior = value;
markNeedsTextLayout(); markNeedsLayout();
} }
/// {@macro flutter.painting.textPainter.textWidthBasis} /// {@macro flutter.painting.textPainter.textWidthBasis}
@ -552,7 +541,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return; return;
} }
_textPainter.textWidthBasis = value; _textPainter.textWidthBasis = value;
markNeedsTextLayout(); markNeedsLayout();
} }
/// The pixel ratio of the current device. /// The pixel ratio of the current device.
@ -565,7 +554,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return; return;
} }
_devicePixelRatio = value; _devicePixelRatio = value;
markNeedsTextLayout(); markNeedsLayout();
} }
/// Character used for obscuring text if [obscureText] is true. /// Character used for obscuring text if [obscureText] is true.
@ -655,7 +644,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
/// {@macro flutter.services.TextLayoutMetrics.getLineAtOffset} /// {@macro flutter.services.TextLayoutMetrics.getLineAtOffset}
@override @override
TextSelection getLineAtOffset(TextPosition position) { TextSelection getLineAtOffset(TextPosition position) {
debugAssertLayoutUpToDate();
final TextRange line = _textPainter.getLineBoundary(position); final TextRange line = _textPainter.getLineBoundary(position);
// If text is obscured, the entire string should be treated as one line. // If text is obscured, the entire string should be treated as one line.
if (obscureText) { if (obscureText) {
@ -761,23 +749,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
_backgroundRenderObject?.markNeedsPaint(); _backgroundRenderObject?.markNeedsPaint();
} }
/// Marks the render object as needing to be laid out again and have its text
/// metrics recomputed.
///
/// Implies [markNeedsLayout].
@protected
void markNeedsTextLayout() {
_textLayoutLastMaxWidth = null;
_textLayoutLastMinWidth = null;
markNeedsLayout();
}
@override @override
void systemFontsDidChange() { void systemFontsDidChange() {
super.systemFontsDidChange(); super.systemFontsDidChange();
_textPainter.markNeedsLayout(); _textPainter.markNeedsLayout();
_textLayoutLastMaxWidth = null;
_textLayoutLastMinWidth = null;
} }
/// Returns a plain text version of the text in [TextPainter]. /// Returns a plain text version of the text in [TextPainter].
@ -803,10 +778,25 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
_cachedAttributedValue = null; _cachedAttributedValue = null;
_cachedCombinedSemanticsInfos = null; _cachedCombinedSemanticsInfos = null;
_canComputeIntrinsicsCached = null; _canComputeIntrinsicsCached = null;
markNeedsTextLayout(); markNeedsLayout();
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
TextPainter? _textIntrinsicsCache;
TextPainter get _textIntrinsics {
return (_textIntrinsicsCache ??= TextPainter())
..text = _textPainter.text
..textAlign = _textPainter.textAlign
..textDirection = _textPainter.textDirection
..textScaler = _textPainter.textScaler
..maxLines = _textPainter.maxLines
..ellipsis = _textPainter.ellipsis
..locale = _textPainter.locale
..strutStyle = _textPainter.strutStyle
..textWidthBasis = _textPainter.textWidthBasis
..textHeightBehavior = _textPainter.textHeightBehavior;
}
/// How the text should be aligned horizontally. /// How the text should be aligned horizontally.
TextAlign get textAlign => _textPainter.textAlign; TextAlign get textAlign => _textPainter.textAlign;
set textAlign(TextAlign value) { set textAlign(TextAlign value) {
@ -814,7 +804,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return; return;
} }
_textPainter.textAlign = value; _textPainter.textAlign = value;
markNeedsTextLayout(); markNeedsLayout();
} }
/// The directionality of the text. /// The directionality of the text.
@ -837,7 +827,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return; return;
} }
_textPainter.textDirection = value; _textPainter.textDirection = value;
markNeedsTextLayout(); markNeedsLayout();
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
@ -857,7 +847,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return; return;
} }
_textPainter.locale = value; _textPainter.locale = value;
markNeedsTextLayout(); markNeedsLayout();
} }
/// The [StrutStyle] used by the renderer's internal [TextPainter] to /// The [StrutStyle] used by the renderer's internal [TextPainter] to
@ -868,7 +858,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return; return;
} }
_textPainter.strutStyle = value; _textPainter.strutStyle = value;
markNeedsTextLayout(); markNeedsLayout();
} }
/// The color to use when painting the cursor. /// The color to use when painting the cursor.
@ -977,7 +967,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
// height of the first line in case there are hard line breaks in the text. // height of the first line in case there are hard line breaks in the text.
// See the `_preferredHeight` method. // See the `_preferredHeight` method.
_textPainter.maxLines = value == 1 ? 1 : null; _textPainter.maxLines = value == 1 ? 1 : null;
markNeedsTextLayout(); markNeedsLayout();
} }
/// {@macro flutter.widgets.editableText.minLines} /// {@macro flutter.widgets.editableText.minLines}
@ -990,7 +980,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return; return;
} }
_minLines = value; _minLines = value;
markNeedsTextLayout(); markNeedsLayout();
} }
/// {@macro flutter.widgets.editableText.expands} /// {@macro flutter.widgets.editableText.expands}
@ -1001,7 +991,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return; return;
} }
_expands = value; _expands = value;
markNeedsTextLayout(); markNeedsLayout();
} }
/// The color to use when painting the selection. /// The color to use when painting the selection.
@ -1039,7 +1029,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return; return;
} }
_textPainter.textScaler = value; _textPainter.textScaler = value;
markNeedsTextLayout(); markNeedsLayout();
} }
/// The region of text that is selected, if any. /// The region of text that is selected, if any.
@ -1215,7 +1205,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return; return;
} }
_enableInteractiveSelection = value; _enableInteractiveSelection = value;
markNeedsTextLayout(); markNeedsLayout();
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
@ -1841,12 +1831,12 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
if (!_canComputeIntrinsics) { if (!_canComputeIntrinsics) {
return 0.0; return 0.0;
} }
_textPainter.setPlaceholderDimensions(layoutInlineChildren( final List<PlaceholderDimensions> placeholderDimensions = layoutInlineChildren(double.infinity, (RenderBox child, BoxConstraints constraints) => Size(child.getMinIntrinsicWidth(double.infinity), 0.0));
double.infinity, final (double minWidth, double maxWidth) = _adjustConstraints();
(RenderBox child, BoxConstraints constraints) => Size(child.getMinIntrinsicWidth(double.infinity), 0.0), return (_textIntrinsics
)); ..setPlaceholderDimensions(placeholderDimensions)
_layoutText(); ..layout(minWidth: minWidth, maxWidth: maxWidth))
return _textPainter.minIntrinsicWidth; .minIntrinsicWidth;
} }
@override @override
@ -1854,14 +1844,17 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
if (!_canComputeIntrinsics) { if (!_canComputeIntrinsics) {
return 0.0; return 0.0;
} }
_textPainter.setPlaceholderDimensions(layoutInlineChildren( final List<PlaceholderDimensions> placeholderDimensions = layoutInlineChildren(
double.infinity, double.infinity,
// Height and baseline is irrelevant as all text will be laid // Height and baseline is irrelevant as all text will be laid
// out in a single line. Therefore, using 0.0 as a dummy for the height. // out in a single line. Therefore, using 0.0 as a dummy for the height.
(RenderBox child, BoxConstraints constraints) => Size(child.getMaxIntrinsicWidth(double.infinity), 0.0), (RenderBox child, BoxConstraints constraints) => Size(child.getMaxIntrinsicWidth(double.infinity), 0.0),
)); );
_layoutText(); final (double minWidth, double maxWidth) = _adjustConstraints();
return _textPainter.maxIntrinsicWidth + _caretMargin; return (_textIntrinsics
..setPlaceholderDimensions(placeholderDimensions)
..layout(minWidth: minWidth, maxWidth: maxWidth))
.maxIntrinsicWidth + _caretMargin;
} }
/// An estimate of the height of a line in the text. See [TextPainter.preferredLineHeight]. /// An estimate of the height of a line in the text. See [TextPainter.preferredLineHeight].
@ -1869,8 +1862,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
double get preferredLineHeight => _textPainter.preferredLineHeight; double get preferredLineHeight => _textPainter.preferredLineHeight;
int? _cachedLineBreakCount; int? _cachedLineBreakCount;
// TODO(LongCatIsLooong): see if we can let ui.Paragraph estimate the number
// of lines
int _countHardLineBreaks(String text) { int _countHardLineBreaks(String text) {
final int? cachedValue = _cachedLineBreakCount; final int? cachedValue = _cachedLineBreakCount;
if (cachedValue != null) { if (cachedValue != null) {
@ -1895,14 +1886,15 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
final int? maxLines = this.maxLines; final int? maxLines = this.maxLines;
final int? minLines = this.minLines ?? maxLines; final int? minLines = this.minLines ?? maxLines;
final double minHeight = preferredLineHeight * (minLines ?? 0); final double minHeight = preferredLineHeight * (minLines ?? 0);
assert(maxLines != 1 || _textIntrinsics.maxLines == 1);
if (maxLines == null) { if (maxLines == null) {
final double estimatedHeight; final double estimatedHeight;
if (width == double.infinity) { if (width == double.infinity) {
estimatedHeight = preferredLineHeight * (_countHardLineBreaks(plainText) + 1); estimatedHeight = preferredLineHeight * (_countHardLineBreaks(plainText) + 1);
} else { } else {
_layoutText(maxWidth: width); final (double minWidth, double maxWidth) = _adjustConstraints(maxWidth: width);
estimatedHeight = _textPainter.height; estimatedHeight = (_textIntrinsics..layout(minWidth: minWidth, maxWidth: maxWidth)).height;
} }
return math.max(estimatedHeight, minHeight); return math.max(estimatedHeight, minHeight);
} }
@ -1914,16 +1906,19 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
// The _layoutText call lays out the paragraph using infinite width when // The _layoutText call lays out the paragraph using infinite width when
// maxLines == 1. Also _textPainter.maxLines will be set to 1 so should // maxLines == 1. Also _textPainter.maxLines will be set to 1 so should
// there be any line breaks only the first line is shown. // there be any line breaks only the first line is shown.
assert(_textPainter.maxLines == 1); final (double minWidth, double maxWidth) = _adjustConstraints(maxWidth: width);
_layoutText(maxWidth: width); return (_textIntrinsics..layout(minWidth: minWidth, maxWidth: maxWidth)).height;
return _textPainter.height;
} }
if (minLines == maxLines) { if (minLines == maxLines) {
return minHeight; return minHeight;
} }
_layoutText(maxWidth: width);
final double maxHeight = preferredLineHeight * maxLines; final double maxHeight = preferredLineHeight * maxLines;
return clampDouble(_textPainter.height, minHeight, maxHeight); final (double minWidth, double maxWidth) = _adjustConstraints(maxWidth: width);
return clampDouble(
(_textIntrinsics..layout(minWidth: minWidth, maxWidth: maxWidth)).height,
minHeight,
maxHeight,
);
} }
@override @override
@ -1934,7 +1929,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
if (!_canComputeIntrinsics) { if (!_canComputeIntrinsics) {
return 0.0; return 0.0;
} }
_textPainter.setPlaceholderDimensions(layoutInlineChildren(width, ChildLayoutHelper.dryLayoutChild)); _textIntrinsics.setPlaceholderDimensions(layoutInlineChildren(width, ChildLayoutHelper.dryLayoutChild));
return _preferredHeight(width); return _preferredHeight(width);
} }
@ -2075,7 +2070,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
/// [from] corresponds to the [TextSelection.baseOffset], and [to] corresponds /// [from] corresponds to the [TextSelection.baseOffset], and [to] corresponds
/// to the [TextSelection.extentOffset]. /// to the [TextSelection.extentOffset].
void selectPositionAt({ required Offset from, Offset? to, required SelectionChangedCause cause }) { void selectPositionAt({ required Offset from, Offset? to, required SelectionChangedCause cause }) {
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); _computeTextMetricsIfNeeded();
final TextPosition fromPosition = _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset)); final TextPosition fromPosition = _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset));
final TextPosition? toPosition = to == null final TextPosition? toPosition = to == null
? null ? null
@ -2150,7 +2145,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
/// [TextPosition]. /// [TextPosition].
@visibleForTesting @visibleForTesting
TextSelection getWordAtOffset(TextPosition position) { TextSelection getWordAtOffset(TextPosition position) {
debugAssertLayoutUpToDate();
// When long-pressing past the end of the text, we want a collapsed cursor. // When long-pressing past the end of the text, we want a collapsed cursor.
if (position.offset >= plainText.length) { if (position.offset >= plainText.length) {
return TextSelection.fromPosition( return TextSelection.fromPosition(
@ -2229,17 +2223,13 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
// restored to the original values before final layout and painting. // restored to the original values before final layout and painting.
List<PlaceholderDimensions>? _placeholderDimensions; List<PlaceholderDimensions>? _placeholderDimensions;
void _layoutText({ double minWidth = 0.0, double maxWidth = double.infinity }) { (double minWidth, double maxWidth) _adjustConstraints({ double minWidth = 0.0, double maxWidth = double.infinity }) {
final double availableMaxWidth = math.max(0.0, maxWidth - _caretMargin); final double availableMaxWidth = math.max(0.0, maxWidth - _caretMargin);
final double availableMinWidth = math.min(minWidth, availableMaxWidth); final double availableMinWidth = math.min(minWidth, availableMaxWidth);
final double textMaxWidth = _isMultiline ? availableMaxWidth : double.infinity; return (
final double textMinWidth = forceLine ? availableMaxWidth : availableMinWidth; forceLine ? availableMaxWidth : availableMinWidth,
_textPainter.layout( _isMultiline ? availableMaxWidth : double.infinity,
minWidth: textMinWidth,
maxWidth: textMaxWidth,
); );
_textLayoutLastMinWidth = minWidth;
_textLayoutLastMaxWidth = maxWidth;
} }
// Computes the text metrics if `_textPainter`'s layout information was marked // Computes the text metrics if `_textPainter`'s layout information was marked
@ -2262,7 +2252,8 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
// the constraints used to layout the `_textPainter` is different. See // the constraints used to layout the `_textPainter` is different. See
// `TextPainter.layout`. // `TextPainter.layout`.
void _computeTextMetricsIfNeeded() { void _computeTextMetricsIfNeeded() {
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); final (double minWidth, double maxWidth) = _adjustConstraints(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
_textPainter.layout(minWidth: minWidth, maxWidth: maxWidth);
} }
late Rect _caretPrototype; late Rect _caretPrototype;
@ -2325,10 +2316,14 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
)); ));
return Size.zero; return Size.zero;
} }
_textPainter.setPlaceholderDimensions(layoutInlineChildren(constraints.maxWidth, ChildLayoutHelper.dryLayoutChild));
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); final (double minWidth, double maxWidth) = _adjustConstraints(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
final double width = forceLine ? constraints.maxWidth : constraints _textIntrinsics
.constrainWidth(_textPainter.size.width + _caretMargin); ..setPlaceholderDimensions(layoutInlineChildren(constraints.maxWidth, ChildLayoutHelper.dryLayoutChild))
..layout(minWidth: minWidth, maxWidth: maxWidth);
final double width = forceLine
? constraints.maxWidth
: constraints.constrainWidth(_textIntrinsics.size.width + _caretMargin);
return Size(width, constraints.constrainHeight(_preferredHeight(constraints.maxWidth))); return Size(width, constraints.constrainHeight(_preferredHeight(constraints.maxWidth)));
} }
@ -2336,8 +2331,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
void performLayout() { void performLayout() {
final BoxConstraints constraints = this.constraints; final BoxConstraints constraints = this.constraints;
_placeholderDimensions = layoutInlineChildren(constraints.maxWidth, ChildLayoutHelper.layoutChild); _placeholderDimensions = layoutInlineChildren(constraints.maxWidth, ChildLayoutHelper.layoutChild);
_textPainter.setPlaceholderDimensions(_placeholderDimensions); final (double minWidth, double maxWidth) = _adjustConstraints(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
_computeTextMetricsIfNeeded(); _textPainter
..setPlaceholderDimensions(_placeholderDimensions)
..layout(minWidth: minWidth, maxWidth: maxWidth);
positionInlineChildren(_textPainter.inlinePlaceholderBoxes!); positionInlineChildren(_textPainter.inlinePlaceholderBoxes!);
_computeCaretPrototype(); _computeCaretPrototype();
// We grab _textPainter.size here because assigning to `size` on the next // We grab _textPainter.size here because assigning to `size` on the next
@ -2349,9 +2346,20 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
// though we currently don't use those here. // though we currently don't use those here.
// See also RenderParagraph which has a similar issue. // See also RenderParagraph which has a similar issue.
final Size textPainterSize = _textPainter.size; final Size textPainterSize = _textPainter.size;
final double width = forceLine ? constraints.maxWidth : constraints final double width = forceLine
.constrainWidth(_textPainter.size.width + _caretMargin); ? constraints.maxWidth
final double preferredHeight = _preferredHeight(constraints.maxWidth); : constraints.constrainWidth(_textPainter.size.width + _caretMargin);
assert(maxLines != 1 || _textPainter.maxLines == 1);
final double preferredHeight = switch (maxLines) {
null => math.max(_textPainter.height, preferredLineHeight * (minLines ?? 0)),
1 => _textPainter.height,
final int maxLines => clampDouble(
_textPainter.height,
preferredLineHeight * (minLines ?? maxLines),
preferredLineHeight * maxLines,
),
};
size = Size(width, constraints.constrainHeight(preferredHeight)); size = Size(width, constraints.constrainHeight(preferredHeight));
final Size contentSize = Size(textPainterSize.width + _caretMargin, textPainterSize.height); final Size contentSize = Size(textPainterSize.width + _caretMargin, textPainterSize.height);
@ -2521,7 +2529,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
} }
void _paintContents(PaintingContext context, Offset offset) { void _paintContents(PaintingContext context, Offset offset) {
debugAssertLayoutUpToDate();
final Offset effectiveOffset = offset + _paintOffset; final Offset effectiveOffset = offset + _paintOffset;
if (selection != null && !_floatingCursorOn) { if (selection != null && !_floatingCursorOn) {

View file

@ -306,6 +306,27 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBo
final TextPainter _textPainter; final TextPainter _textPainter;
// Currently, computing min/max intrinsic width/height will destroy state
// inside the painter. Instead of calling _layout again to get back the correct
// state, use a separate TextPainter for intrinsics calculation.
//
// TODO(abarth): Make computing the min/max intrinsic width/height a
// non-destructive operation.
TextPainter? _textIntrinsicsCache;
TextPainter get _textIntrinsics {
return (_textIntrinsicsCache ??= TextPainter())
..text = _textPainter.text
..textAlign = _textPainter.textAlign
..textDirection = _textPainter.textDirection
..textScaler = _textPainter.textScaler
..maxLines = _textPainter.maxLines
..ellipsis = _textPainter.ellipsis
..locale = _textPainter.locale
..strutStyle = _textPainter.strutStyle
..textWidthBasis = _textPainter.textWidthBasis
..textHeightBehavior = _textPainter.textHeightBehavior;
}
List<AttributedString>? _cachedAttributedLabels; List<AttributedString>? _cachedAttributedLabels;
List<InlineSpanSemanticsInformation>? _cachedCombinedSemanticsInfos; List<InlineSpanSemanticsInformation>? _cachedCombinedSemanticsInfos;
@ -448,6 +469,7 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBo
_removeSelectionRegistrarSubscription(); _removeSelectionRegistrarSubscription();
_disposeSelectableFragments(); _disposeSelectableFragments();
_textPainter.dispose(); _textPainter.dispose();
_textIntrinsicsCache?.dispose();
super.dispose(); super.dispose();
} }
@ -630,21 +652,17 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBo
return getOffsetForCaret(position, Rect.zero) + Offset(0, getFullHeightForCaret(position) ?? 0.0); return getOffsetForCaret(position, Rect.zero) + Offset(0, getFullHeightForCaret(position) ?? 0.0);
} }
List<ui.LineMetrics> _computeLineMetrics() {
return _textPainter.computeLineMetrics();
}
@override @override
double computeMinIntrinsicWidth(double height) { double computeMinIntrinsicWidth(double height) {
if (!_canComputeIntrinsics()) { if (!_canComputeIntrinsics()) {
return 0.0; return 0.0;
} }
_textPainter.setPlaceholderDimensions(layoutInlineChildren( final List<PlaceholderDimensions> placeholderDimensions = layoutInlineChildren(
double.infinity, double.infinity,
(RenderBox child, BoxConstraints constraints) => Size(child.getMinIntrinsicWidth(double.infinity), 0.0), (RenderBox child, BoxConstraints constraints) => Size(child.getMinIntrinsicWidth(double.infinity), 0.0),
)); );
_layoutText(); // layout with infinite width. return (_textIntrinsics..setPlaceholderDimensions(placeholderDimensions)..layout())
return _textPainter.minIntrinsicWidth; .minIntrinsicWidth;
} }
@override @override
@ -652,23 +670,24 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBo
if (!_canComputeIntrinsics()) { if (!_canComputeIntrinsics()) {
return 0.0; return 0.0;
} }
_textPainter.setPlaceholderDimensions(layoutInlineChildren( final List<PlaceholderDimensions> placeholderDimensions = layoutInlineChildren(
double.infinity, double.infinity,
// Height and baseline is irrelevant as all text will be laid // Height and baseline is irrelevant as all text will be laid
// out in a single line. Therefore, using 0.0 as a dummy for the height. // out in a single line. Therefore, using 0.0 as a dummy for the height.
(RenderBox child, BoxConstraints constraints) => Size(child.getMaxIntrinsicWidth(double.infinity), 0.0), (RenderBox child, BoxConstraints constraints) => Size(child.getMaxIntrinsicWidth(double.infinity), 0.0),
)); );
_layoutText(); // layout with infinite width. return (_textIntrinsics..setPlaceholderDimensions(placeholderDimensions)..layout())
return _textPainter.maxIntrinsicWidth; .maxIntrinsicWidth;
} }
double _computeIntrinsicHeight(double width) { double _computeIntrinsicHeight(double width) {
if (!_canComputeIntrinsics()) { if (!_canComputeIntrinsics()) {
return 0.0; return 0.0;
} }
_textPainter.setPlaceholderDimensions(layoutInlineChildren(width, ChildLayoutHelper.dryLayoutChild)); return (_textIntrinsics
_layoutText(minWidth: width, maxWidth: width); ..setPlaceholderDimensions(layoutInlineChildren(width, ChildLayoutHelper.dryLayoutChild))
return _textPainter.height; ..layout(minWidth: width, maxWidth: _adjustMaxWidth(width)))
.height;
} }
@override @override
@ -761,14 +780,6 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBo
@visibleForTesting @visibleForTesting
bool get debugHasOverflowShader => _overflowShader != null; bool get debugHasOverflowShader => _overflowShader != null;
void _layoutText({ double minWidth = 0.0, double maxWidth = double.infinity }) {
final bool widthMatters = softWrap || overflow == TextOverflow.ellipsis;
_textPainter.layout(
minWidth: minWidth,
maxWidth: widthMatters ? maxWidth : double.infinity,
);
}
@override @override
void systemFontsDidChange() { void systemFontsDidChange() {
super.systemFontsDidChange(); super.systemFontsDidChange();
@ -782,9 +793,13 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBo
// restored to the original values before final layout and painting. // restored to the original values before final layout and painting.
List<PlaceholderDimensions>? _placeholderDimensions; List<PlaceholderDimensions>? _placeholderDimensions;
double _adjustMaxWidth(double maxWidth) {
return softWrap || overflow == TextOverflow.ellipsis ? maxWidth : double.infinity;
}
void _layoutTextWithConstraints(BoxConstraints constraints) { void _layoutTextWithConstraints(BoxConstraints constraints) {
_textPainter.setPlaceholderDimensions(_placeholderDimensions); _textPainter
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); ..setPlaceholderDimensions(_placeholderDimensions)
..layout(minWidth: constraints.minWidth, maxWidth: _adjustMaxWidth(constraints.maxWidth));
} }
@override @override
@ -796,9 +811,11 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBo
)); ));
return Size.zero; return Size.zero;
} }
_textPainter.setPlaceholderDimensions(layoutInlineChildren(constraints.maxWidth, ChildLayoutHelper.dryLayoutChild)); final Size size = (_textIntrinsics
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); ..setPlaceholderDimensions(layoutInlineChildren(constraints.maxWidth, ChildLayoutHelper.dryLayoutChild))
return constraints.constrain(_textPainter.size); ..layout(minWidth: constraints.minWidth, maxWidth: _adjustMaxWidth(constraints.maxWidth)))
.size;
return constraints.constrain(size);
} }
@override @override
@ -876,18 +893,10 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBo
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
// Ideally we could compute the min/max intrinsic width/height with a // Text alignment only triggers repaint so it's possible the text layout has
// non-destructive operation. However, currently, computing these values // been invalidated but performLayout wasn't called at this point. Make sure
// will destroy state inside the painter. If that happens, we need to get // the TextPainter has a valid layout.
// back the correct state by calling _layout again.
//
// TODO(abarth): Make computing the min/max intrinsic width/height a
// non-destructive operation.
//
// If you remove this call, make sure that changing the textAlign still
// works properly.
_layoutTextWithConstraints(constraints); _layoutTextWithConstraints(constraints);
assert(() { assert(() {
if (debugRepaintTextRainbowEnabled) { if (debugRepaintTextRainbowEnabled) {
final Paint paint = Paint() final Paint paint = Paint()
@ -1919,7 +1928,7 @@ class _SelectableFragment with Selectable, Diagnosticable, ChangeNotifier implem
} }
MapEntry<TextPosition, SelectionResult> _handleVerticalMovement(TextPosition position, {required double horizontalBaselineInParagraphCoordinates, required bool below}) { MapEntry<TextPosition, SelectionResult> _handleVerticalMovement(TextPosition position, {required double horizontalBaselineInParagraphCoordinates, required bool below}) {
final List<ui.LineMetrics> lines = paragraph._computeLineMetrics(); final List<ui.LineMetrics> lines = paragraph._textPainter.computeLineMetrics();
final Offset offset = paragraph.getOffsetForCaret(position, Rect.zero); final Offset offset = paragraph.getOffsetForCaret(position, Rect.zero);
int currentLine = lines.length - 1; int currentLine = lines.length - 1;
for (final ui.LineMetrics lineMetrics in lines) { for (final ui.LineMetrics lineMetrics in lines) {

View file

@ -0,0 +1,147 @@
// 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 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
final TextSelectionDelegate delegate = _FakeEditableTextState();
test('editable intrinsics', () {
final RenderEditable editable = RenderEditable(
text: const TextSpan(
style: TextStyle(height: 1.0, fontSize: 10.0),
text: '12345',
),
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
textDirection: TextDirection.ltr,
locale: const Locale('ja', 'JP'),
offset: ViewportOffset.zero(),
textSelectionDelegate: delegate,
);
expect(editable.getMinIntrinsicWidth(double.infinity), 50.0);
// The width includes the width of the cursor (1.0).
expect(editable.getMaxIntrinsicWidth(double.infinity), 52.0);
expect(editable.getMinIntrinsicHeight(double.infinity), 10.0);
expect(editable.getMaxIntrinsicHeight(double.infinity), 10.0);
expect(
editable.toStringDeep(minLevel: DiagnosticLevel.info),
equalsIgnoringHashCodes(
'RenderEditable#00000 NEEDS-LAYOUT NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE DETACHED\n'
' │ parentData: MISSING\n'
' │ constraints: MISSING\n'
' │ size: MISSING\n'
' │ cursorColor: null\n'
' │ showCursor: ValueNotifier<bool>#00000(false)\n'
' │ maxLines: 1\n'
' │ minLines: null\n'
' │ selectionColor: null\n'
' │ locale: ja_JP\n'
' │ selection: null\n'
' │ offset: _FixedViewportOffset#00000(offset: 0.0)\n'
' ╘═╦══ text ═══\n'
' ║ TextSpan:\n'
' ║ inherit: true\n'
' ║ size: 10.0\n'
' ║ height: 1.0x\n'
' ║ "12345"\n'
' ╚═══════════\n',
),
);
});
test('textScaler affects intrinsics', () {
final RenderEditable editable = RenderEditable(
text: const TextSpan(
style: TextStyle(fontSize: 10),
text: 'Hello World',
),
textDirection: TextDirection.ltr,
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
offset: ViewportOffset.zero(),
textSelectionDelegate: delegate,
);
expect(editable.getMaxIntrinsicWidth(double.infinity), 110 + 2);
editable.textScaler = const TextScaler.linear(2);
expect(editable.getMaxIntrinsicWidth(double.infinity), 220 + 2);
});
test('maxLines affects intrinsics', () {
final RenderEditable editable = RenderEditable(
text: TextSpan(
style: const TextStyle(fontSize: 10),
text: List<String>.filled(5, 'A').join('\n'),
),
textDirection: TextDirection.ltr,
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
offset: ViewportOffset.zero(),
textSelectionDelegate: delegate,
maxLines: null,
);
expect(editable.getMaxIntrinsicHeight(double.infinity), 50);
editable.maxLines = 1;
expect(editable.getMaxIntrinsicHeight(double.infinity), 10);
});
test('strutStyle affects intrinsics', () {
final RenderEditable editable = RenderEditable(
text: const TextSpan(
style: TextStyle(fontSize: 10),
text: 'Hello World',
),
textDirection: TextDirection.ltr,
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
offset: ViewportOffset.zero(),
textSelectionDelegate: delegate,
);
expect(editable.getMaxIntrinsicHeight(double.infinity), 10);
editable.strutStyle = const StrutStyle(fontSize: 100, forceStrutHeight: true);
expect(editable.getMaxIntrinsicHeight(double.infinity), 100);
}, skip: kIsWeb && !isCanvasKit); // [intended] strut spport for HTML renderer https://github.com/flutter/flutter/issues/32243.
}
class _FakeEditableTextState with TextSelectionDelegate {
@override
TextEditingValue textEditingValue = TextEditingValue.empty;
TextSelection? selection;
@override
void hideToolbar([bool hideHandles = true]) { }
@override
void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause) {
selection = value.selection;
}
@override
void bringIntoView(TextPosition position) { }
@override
void cutSelection(SelectionChangedCause cause) { }
@override
Future<void> pasteText(SelectionChangedCause cause) {
return Future<void>.value();
}
@override
void selectAll(SelectionChangedCause cause) { }
@override
void copySelection(SelectionChangedCause cause) { }
}

View file

@ -201,52 +201,6 @@ void main() {
expect(leaderLayers.single.offset, endpoint + paintOffset, reason: 'offset should respect paintOffset'); expect(leaderLayers.single.offset, endpoint + paintOffset, reason: 'offset should respect paintOffset');
}); });
test('editable intrinsics', () {
final TextSelectionDelegate delegate = _FakeEditableTextState();
final RenderEditable editable = RenderEditable(
text: const TextSpan(
style: TextStyle(height: 1.0, fontSize: 10.0),
text: '12345',
),
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
textDirection: TextDirection.ltr,
locale: const Locale('ja', 'JP'),
offset: ViewportOffset.zero(),
textSelectionDelegate: delegate,
);
expect(editable.getMinIntrinsicWidth(double.infinity), 50.0);
// The width includes the width of the cursor (1.0).
expect(editable.getMaxIntrinsicWidth(double.infinity), 52.0);
expect(editable.getMinIntrinsicHeight(double.infinity), 10.0);
expect(editable.getMaxIntrinsicHeight(double.infinity), 10.0);
expect(
editable.toStringDeep(minLevel: DiagnosticLevel.info),
equalsIgnoringHashCodes(
'RenderEditable#00000 NEEDS-LAYOUT NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE DETACHED\n'
' │ parentData: MISSING\n'
' │ constraints: MISSING\n'
' │ size: MISSING\n'
' │ cursorColor: null\n'
' │ showCursor: ValueNotifier<bool>#00000(false)\n'
' │ maxLines: 1\n'
' │ minLines: null\n'
' │ selectionColor: null\n'
' │ locale: ja_JP\n'
' │ selection: null\n'
' │ offset: _FixedViewportOffset#00000(offset: 0.0)\n'
' ╘═╦══ text ═══\n'
' ║ TextSpan:\n'
' ║ inherit: true\n'
' ║ size: 10.0\n'
' ║ height: 1.0x\n'
' ║ "12345"\n'
' ╚═══════════\n',
),
);
});
// Test that clipping will be used even when the text fits within the visible // Test that clipping will be used even when the text fits within the visible
// region if the start position of the text is offset (e.g. during scrolling // region if the start position of the text is offset (e.g. during scrolling
// animation). // animation).

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -66,4 +67,49 @@ void main() {
expect(testBlock.getMinIntrinsicHeight(0.0), equals(manyLinesTextHeight)); expect(testBlock.getMinIntrinsicHeight(0.0), equals(manyLinesTextHeight));
expect(testBlock.getMaxIntrinsicHeight(0.0), equals(manyLinesTextHeight)); expect(testBlock.getMaxIntrinsicHeight(0.0), equals(manyLinesTextHeight));
}); });
test('textScaler affects intrinsics', () {
final RenderParagraph paragraph = RenderParagraph(
const TextSpan(
style: TextStyle(fontSize: 10),
text: 'Hello World',
),
textDirection: TextDirection.ltr,
);
expect(paragraph.getMaxIntrinsicWidth(double.infinity), 110);
paragraph.textScaler = const TextScaler.linear(2);
expect(paragraph.getMaxIntrinsicWidth(double.infinity), 220);
});
test('maxLines affects intrinsics', () {
final RenderParagraph paragraph = RenderParagraph(
TextSpan(
style: const TextStyle(fontSize: 10),
text: List<String>.filled(5, 'A').join('\n'),
),
textDirection: TextDirection.ltr,
);
expect(paragraph.getMaxIntrinsicHeight(double.infinity), 50);
paragraph.maxLines = 1;
expect(paragraph.getMaxIntrinsicHeight(double.infinity), 10);
});
test('strutStyle affects intrinsics', () {
final RenderParagraph paragraph = RenderParagraph(
const TextSpan(
style: TextStyle(fontSize: 10),
text: 'Hello World',
),
textDirection: TextDirection.ltr,
);
expect(paragraph.getMaxIntrinsicHeight(double.infinity), 10);
paragraph.strutStyle = const StrutStyle(fontSize: 100, forceStrutHeight: true);
expect(paragraph.getMaxIntrinsicHeight(double.infinity), 100);
}, skip: kIsWeb && !isCanvasKit); // [intended] strut spport for HTML renderer https://github.com/flutter/flutter/issues/32243.
} }

View file

@ -375,6 +375,27 @@ void main() {
expect(paragraph.size.height, 30.0); expect(paragraph.size.height, 30.0);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61018 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61018
test('textAlign triggers TextPainter relayout in the paint method', () {
final RenderParagraph paragraph = RenderParagraph(
const TextSpan(text: 'A', style: TextStyle(fontSize: 10.0)),
textDirection: TextDirection.ltr,
textAlign: TextAlign.left,
);
Rect getRectForA() => paragraph.getBoxesForSelection(const TextSelection(baseOffset: 0, extentOffset: 1)).single.toRect();
layout(paragraph, constraints: const BoxConstraints.tightFor(width: 100.0));
expect(getRectForA(), const Rect.fromLTWH(0, 0, 10, 10));
paragraph.textAlign = TextAlign.right;
expect(paragraph.debugNeedsLayout, isFalse);
expect(paragraph.debugNeedsPaint, isTrue);
paragraph.paint(MockPaintingContext(), Offset.zero);
expect(getRectForA(), const Rect.fromLTWH(90, 0, 10, 10));
});
group('didExceedMaxLines', () { group('didExceedMaxLines', () {
RenderParagraph createRenderParagraph({ RenderParagraph createRenderParagraph({
int? maxLines, int? maxLines,