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();
_caretPainter.dispose();
_textPainter.dispose();
_textIntrinsicsCache?.dispose();
if (_disposeShowCursor) {
_showCursor.dispose();
_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
/// handlers.
///
@ -542,7 +531,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return;
}
_textPainter.textHeightBehavior = value;
markNeedsTextLayout();
markNeedsLayout();
}
/// {@macro flutter.painting.textPainter.textWidthBasis}
@ -552,7 +541,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return;
}
_textPainter.textWidthBasis = value;
markNeedsTextLayout();
markNeedsLayout();
}
/// The pixel ratio of the current device.
@ -565,7 +554,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return;
}
_devicePixelRatio = value;
markNeedsTextLayout();
markNeedsLayout();
}
/// Character used for obscuring text if [obscureText] is true.
@ -655,7 +644,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
/// {@macro flutter.services.TextLayoutMetrics.getLineAtOffset}
@override
TextSelection getLineAtOffset(TextPosition position) {
debugAssertLayoutUpToDate();
final TextRange line = _textPainter.getLineBoundary(position);
// If text is obscured, the entire string should be treated as one line.
if (obscureText) {
@ -761,23 +749,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
_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
void systemFontsDidChange() {
super.systemFontsDidChange();
_textPainter.markNeedsLayout();
_textLayoutLastMaxWidth = null;
_textLayoutLastMinWidth = null;
}
/// Returns a plain text version of the text in [TextPainter].
@ -803,10 +778,25 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
_cachedAttributedValue = null;
_cachedCombinedSemanticsInfos = null;
_canComputeIntrinsicsCached = null;
markNeedsTextLayout();
markNeedsLayout();
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.
TextAlign get textAlign => _textPainter.textAlign;
set textAlign(TextAlign value) {
@ -814,7 +804,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return;
}
_textPainter.textAlign = value;
markNeedsTextLayout();
markNeedsLayout();
}
/// The directionality of the text.
@ -837,7 +827,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return;
}
_textPainter.textDirection = value;
markNeedsTextLayout();
markNeedsLayout();
markNeedsSemanticsUpdate();
}
@ -857,7 +847,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return;
}
_textPainter.locale = value;
markNeedsTextLayout();
markNeedsLayout();
}
/// The [StrutStyle] used by the renderer's internal [TextPainter] to
@ -868,7 +858,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return;
}
_textPainter.strutStyle = value;
markNeedsTextLayout();
markNeedsLayout();
}
/// 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.
// See the `_preferredHeight` method.
_textPainter.maxLines = value == 1 ? 1 : null;
markNeedsTextLayout();
markNeedsLayout();
}
/// {@macro flutter.widgets.editableText.minLines}
@ -990,7 +980,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return;
}
_minLines = value;
markNeedsTextLayout();
markNeedsLayout();
}
/// {@macro flutter.widgets.editableText.expands}
@ -1001,7 +991,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return;
}
_expands = value;
markNeedsTextLayout();
markNeedsLayout();
}
/// The color to use when painting the selection.
@ -1039,7 +1029,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return;
}
_textPainter.textScaler = value;
markNeedsTextLayout();
markNeedsLayout();
}
/// The region of text that is selected, if any.
@ -1215,7 +1205,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return;
}
_enableInteractiveSelection = value;
markNeedsTextLayout();
markNeedsLayout();
markNeedsSemanticsUpdate();
}
@ -1841,12 +1831,12 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
if (!_canComputeIntrinsics) {
return 0.0;
}
_textPainter.setPlaceholderDimensions(layoutInlineChildren(
double.infinity,
(RenderBox child, BoxConstraints constraints) => Size(child.getMinIntrinsicWidth(double.infinity), 0.0),
));
_layoutText();
return _textPainter.minIntrinsicWidth;
final List<PlaceholderDimensions> placeholderDimensions = layoutInlineChildren(double.infinity, (RenderBox child, BoxConstraints constraints) => Size(child.getMinIntrinsicWidth(double.infinity), 0.0));
final (double minWidth, double maxWidth) = _adjustConstraints();
return (_textIntrinsics
..setPlaceholderDimensions(placeholderDimensions)
..layout(minWidth: minWidth, maxWidth: maxWidth))
.minIntrinsicWidth;
}
@override
@ -1854,14 +1844,17 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
if (!_canComputeIntrinsics) {
return 0.0;
}
_textPainter.setPlaceholderDimensions(layoutInlineChildren(
final List<PlaceholderDimensions> placeholderDimensions = layoutInlineChildren(
double.infinity,
// 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.
(RenderBox child, BoxConstraints constraints) => Size(child.getMaxIntrinsicWidth(double.infinity), 0.0),
));
_layoutText();
return _textPainter.maxIntrinsicWidth + _caretMargin;
);
final (double minWidth, double maxWidth) = _adjustConstraints();
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].
@ -1869,8 +1862,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
double get preferredLineHeight => _textPainter.preferredLineHeight;
int? _cachedLineBreakCount;
// TODO(LongCatIsLooong): see if we can let ui.Paragraph estimate the number
// of lines
int _countHardLineBreaks(String text) {
final int? cachedValue = _cachedLineBreakCount;
if (cachedValue != null) {
@ -1895,14 +1886,15 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
final int? maxLines = this.maxLines;
final int? minLines = this.minLines ?? maxLines;
final double minHeight = preferredLineHeight * (minLines ?? 0);
assert(maxLines != 1 || _textIntrinsics.maxLines == 1);
if (maxLines == null) {
final double estimatedHeight;
if (width == double.infinity) {
estimatedHeight = preferredLineHeight * (_countHardLineBreaks(plainText) + 1);
} else {
_layoutText(maxWidth: width);
estimatedHeight = _textPainter.height;
final (double minWidth, double maxWidth) = _adjustConstraints(maxWidth: width);
estimatedHeight = (_textIntrinsics..layout(minWidth: minWidth, maxWidth: maxWidth)).height;
}
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
// maxLines == 1. Also _textPainter.maxLines will be set to 1 so should
// there be any line breaks only the first line is shown.
assert(_textPainter.maxLines == 1);
_layoutText(maxWidth: width);
return _textPainter.height;
final (double minWidth, double maxWidth) = _adjustConstraints(maxWidth: width);
return (_textIntrinsics..layout(minWidth: minWidth, maxWidth: maxWidth)).height;
}
if (minLines == maxLines) {
return minHeight;
}
_layoutText(maxWidth: width);
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
@ -1934,7 +1929,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
if (!_canComputeIntrinsics) {
return 0.0;
}
_textPainter.setPlaceholderDimensions(layoutInlineChildren(width, ChildLayoutHelper.dryLayoutChild));
_textIntrinsics.setPlaceholderDimensions(layoutInlineChildren(width, ChildLayoutHelper.dryLayoutChild));
return _preferredHeight(width);
}
@ -2075,7 +2070,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
/// [from] corresponds to the [TextSelection.baseOffset], and [to] corresponds
/// to the [TextSelection.extentOffset].
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? toPosition = to == null
? null
@ -2150,7 +2145,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
/// [TextPosition].
@visibleForTesting
TextSelection getWordAtOffset(TextPosition position) {
debugAssertLayoutUpToDate();
// When long-pressing past the end of the text, we want a collapsed cursor.
if (position.offset >= plainText.length) {
return TextSelection.fromPosition(
@ -2229,17 +2223,13 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
// restored to the original values before final layout and painting.
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 availableMinWidth = math.min(minWidth, availableMaxWidth);
final double textMaxWidth = _isMultiline ? availableMaxWidth : double.infinity;
final double textMinWidth = forceLine ? availableMaxWidth : availableMinWidth;
_textPainter.layout(
minWidth: textMinWidth,
maxWidth: textMaxWidth,
return (
forceLine ? availableMaxWidth : availableMinWidth,
_isMultiline ? availableMaxWidth : double.infinity,
);
_textLayoutLastMinWidth = minWidth;
_textLayoutLastMaxWidth = maxWidth;
}
// 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
// `TextPainter.layout`.
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;
@ -2325,10 +2316,14 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
));
return Size.zero;
}
_textPainter.setPlaceholderDimensions(layoutInlineChildren(constraints.maxWidth, ChildLayoutHelper.dryLayoutChild));
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
final double width = forceLine ? constraints.maxWidth : constraints
.constrainWidth(_textPainter.size.width + _caretMargin);
final (double minWidth, double maxWidth) = _adjustConstraints(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
_textIntrinsics
..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)));
}
@ -2336,8 +2331,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
void performLayout() {
final BoxConstraints constraints = this.constraints;
_placeholderDimensions = layoutInlineChildren(constraints.maxWidth, ChildLayoutHelper.layoutChild);
_textPainter.setPlaceholderDimensions(_placeholderDimensions);
_computeTextMetricsIfNeeded();
final (double minWidth, double maxWidth) = _adjustConstraints(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
_textPainter
..setPlaceholderDimensions(_placeholderDimensions)
..layout(minWidth: minWidth, maxWidth: maxWidth);
positionInlineChildren(_textPainter.inlinePlaceholderBoxes!);
_computeCaretPrototype();
// 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.
// See also RenderParagraph which has a similar issue.
final Size textPainterSize = _textPainter.size;
final double width = forceLine ? constraints.maxWidth : constraints
.constrainWidth(_textPainter.size.width + _caretMargin);
final double preferredHeight = _preferredHeight(constraints.maxWidth);
final double width = forceLine
? 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));
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) {
debugAssertLayoutUpToDate();
final Offset effectiveOffset = offset + _paintOffset;
if (selection != null && !_floatingCursorOn) {

View file

@ -306,6 +306,27 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBo
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<InlineSpanSemanticsInformation>? _cachedCombinedSemanticsInfos;
@ -448,6 +469,7 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBo
_removeSelectionRegistrarSubscription();
_disposeSelectableFragments();
_textPainter.dispose();
_textIntrinsicsCache?.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);
}
List<ui.LineMetrics> _computeLineMetrics() {
return _textPainter.computeLineMetrics();
}
@override
double computeMinIntrinsicWidth(double height) {
if (!_canComputeIntrinsics()) {
return 0.0;
}
_textPainter.setPlaceholderDimensions(layoutInlineChildren(
final List<PlaceholderDimensions> placeholderDimensions = layoutInlineChildren(
double.infinity,
(RenderBox child, BoxConstraints constraints) => Size(child.getMinIntrinsicWidth(double.infinity), 0.0),
));
_layoutText(); // layout with infinite width.
return _textPainter.minIntrinsicWidth;
);
return (_textIntrinsics..setPlaceholderDimensions(placeholderDimensions)..layout())
.minIntrinsicWidth;
}
@override
@ -652,23 +670,24 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBo
if (!_canComputeIntrinsics()) {
return 0.0;
}
_textPainter.setPlaceholderDimensions(layoutInlineChildren(
final List<PlaceholderDimensions> placeholderDimensions = layoutInlineChildren(
double.infinity,
// 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.
(RenderBox child, BoxConstraints constraints) => Size(child.getMaxIntrinsicWidth(double.infinity), 0.0),
));
_layoutText(); // layout with infinite width.
return _textPainter.maxIntrinsicWidth;
);
return (_textIntrinsics..setPlaceholderDimensions(placeholderDimensions)..layout())
.maxIntrinsicWidth;
}
double _computeIntrinsicHeight(double width) {
if (!_canComputeIntrinsics()) {
return 0.0;
}
_textPainter.setPlaceholderDimensions(layoutInlineChildren(width, ChildLayoutHelper.dryLayoutChild));
_layoutText(minWidth: width, maxWidth: width);
return _textPainter.height;
return (_textIntrinsics
..setPlaceholderDimensions(layoutInlineChildren(width, ChildLayoutHelper.dryLayoutChild))
..layout(minWidth: width, maxWidth: _adjustMaxWidth(width)))
.height;
}
@override
@ -761,14 +780,6 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBo
@visibleForTesting
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
void systemFontsDidChange() {
super.systemFontsDidChange();
@ -782,9 +793,13 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBo
// restored to the original values before final layout and painting.
List<PlaceholderDimensions>? _placeholderDimensions;
double _adjustMaxWidth(double maxWidth) {
return softWrap || overflow == TextOverflow.ellipsis ? maxWidth : double.infinity;
}
void _layoutTextWithConstraints(BoxConstraints constraints) {
_textPainter.setPlaceholderDimensions(_placeholderDimensions);
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
_textPainter
..setPlaceholderDimensions(_placeholderDimensions)
..layout(minWidth: constraints.minWidth, maxWidth: _adjustMaxWidth(constraints.maxWidth));
}
@override
@ -796,9 +811,11 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBo
));
return Size.zero;
}
_textPainter.setPlaceholderDimensions(layoutInlineChildren(constraints.maxWidth, ChildLayoutHelper.dryLayoutChild));
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
return constraints.constrain(_textPainter.size);
final Size size = (_textIntrinsics
..setPlaceholderDimensions(layoutInlineChildren(constraints.maxWidth, ChildLayoutHelper.dryLayoutChild))
..layout(minWidth: constraints.minWidth, maxWidth: _adjustMaxWidth(constraints.maxWidth)))
.size;
return constraints.constrain(size);
}
@override
@ -876,18 +893,10 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBo
@override
void paint(PaintingContext context, Offset offset) {
// Ideally we could compute the min/max intrinsic width/height with a
// non-destructive operation. However, currently, computing these values
// will destroy state inside the painter. If that happens, we need to get
// 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.
// Text alignment only triggers repaint so it's possible the text layout has
// been invalidated but performLayout wasn't called at this point. Make sure
// the TextPainter has a valid layout.
_layoutTextWithConstraints(constraints);
assert(() {
if (debugRepaintTextRainbowEnabled) {
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}) {
final List<ui.LineMetrics> lines = paragraph._computeLineMetrics();
final List<ui.LineMetrics> lines = paragraph._textPainter.computeLineMetrics();
final Offset offset = paragraph.getOffsetForCaret(position, Rect.zero);
int currentLine = lines.length - 1;
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');
});
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
// region if the start position of the text is offset (e.g. during scrolling
// animation).

View file

@ -2,6 +2,7 @@
// 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_test/flutter_test.dart';
@ -66,4 +67,49 @@ void main() {
expect(testBlock.getMinIntrinsicHeight(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);
}, 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', () {
RenderParagraph createRenderParagraph({
int? maxLines,