From cc01701781f85be4e738e18a82218f6b3610f695 Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Fri, 15 Mar 2024 18:15:19 -0700 Subject: [PATCH] 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](https://github.com/google/skia/blob/9c62e7b382cf387195ef82895530c97ccceda690/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. --- .../flutter/lib/src/rendering/editable.dart | 167 +++++++++--------- .../flutter/lib/src/rendering/paragraph.dart | 89 +++++----- .../rendering/editable_intrinsics_test.dart | 147 +++++++++++++++ .../flutter/test/rendering/editable_test.dart | 46 ----- .../rendering/paragraph_intrinsics_test.dart | 46 +++++ .../test/rendering/paragraph_test.dart | 21 +++ 6 files changed, 350 insertions(+), 166 deletions(-) create mode 100644 packages/flutter/test/rendering/editable_intrinsics_test.dart diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart index 3e1587bc503..71703cad4bd 100644 --- a/packages/flutter/lib/src/rendering/editable.dart +++ b/packages/flutter/lib/src/rendering/editable.dart @@ -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 = 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 = 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; - 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) { diff --git a/packages/flutter/lib/src/rendering/paragraph.dart b/packages/flutter/lib/src/rendering/paragraph.dart index 681b690ced8..b1ee1029408 100644 --- a/packages/flutter/lib/src/rendering/paragraph.dart +++ b/packages/flutter/lib/src/rendering/paragraph.dart @@ -306,6 +306,27 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin? _cachedAttributedLabels; List? _cachedCombinedSemanticsInfos; @@ -448,6 +469,7 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin _computeLineMetrics() { - return _textPainter.computeLineMetrics(); - } - @override double computeMinIntrinsicWidth(double height) { if (!_canComputeIntrinsics()) { return 0.0; } - _textPainter.setPlaceholderDimensions(layoutInlineChildren( + final List 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 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 _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? _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 _handleVerticalMovement(TextPosition position, {required double horizontalBaselineInParagraphCoordinates, required bool below}) { - final List lines = paragraph._computeLineMetrics(); + final List lines = paragraph._textPainter.computeLineMetrics(); final Offset offset = paragraph.getOffsetForCaret(position, Rect.zero); int currentLine = lines.length - 1; for (final ui.LineMetrics lineMetrics in lines) { diff --git a/packages/flutter/test/rendering/editable_intrinsics_test.dart b/packages/flutter/test/rendering/editable_intrinsics_test.dart new file mode 100644 index 00000000000..3990a8a9fa0 --- /dev/null +++ b/packages/flutter/test/rendering/editable_intrinsics_test.dart @@ -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#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.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 pasteText(SelectionChangedCause cause) { + return Future.value(); + } + + @override + void selectAll(SelectionChangedCause cause) { } + + @override + void copySelection(SelectionChangedCause cause) { } +} diff --git a/packages/flutter/test/rendering/editable_test.dart b/packages/flutter/test/rendering/editable_test.dart index 44bbd34b4e0..8190e178cbb 100644 --- a/packages/flutter/test/rendering/editable_test.dart +++ b/packages/flutter/test/rendering/editable_test.dart @@ -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#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). diff --git a/packages/flutter/test/rendering/paragraph_intrinsics_test.dart b/packages/flutter/test/rendering/paragraph_intrinsics_test.dart index cc3c7a68b6f..a4032835f0b 100644 --- a/packages/flutter/test/rendering/paragraph_intrinsics_test.dart +++ b/packages/flutter/test/rendering/paragraph_intrinsics_test.dart @@ -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.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. } diff --git a/packages/flutter/test/rendering/paragraph_test.dart b/packages/flutter/test/rendering/paragraph_test.dart index ad9e0802325..86069dacda1 100644 --- a/packages/flutter/test/rendering/paragraph_test.dart +++ b/packages/flutter/test/rendering/paragraph_test.dart @@ -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,