Unverified Commit 2b9fdada authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

[RenderEditable] fix crash & remove `TextPainter.layout` short-circuiting (#85008)

parent 9a981b91
......@@ -301,6 +301,8 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
_foregroundRenderObject = null;
_backgroundRenderObject?.dispose();
_backgroundRenderObject = null;
_cachedBuiltInForegroundPainters?.dispose();
_cachedBuiltInPainters?.dispose();
super.dispose();
}
......@@ -3162,8 +3164,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
/// * [getLocalRectForCaret], which is the equivalent but for
/// a [TextPosition] rather than a [TextSelection].
List<TextSelectionPoint> getEndpointsForSelection(TextSelection selection) {
assert(constraints != null);
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
_computeTextMetricsIfNeeded();
final Offset paintOffset = _paintOffset;
......@@ -3193,10 +3194,9 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
/// Returns null if [TextRange.isValid] is false for the given `range`, or the
/// given `range` is collapsed.
Rect? getRectForComposingRange(TextRange range) {
assert(constraints != null);
if (!range.isValid || range.isCollapsed)
return null;
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
_computeTextMetricsIfNeeded();
final List<ui.TextBox> boxes = _textPainter.getBoxesForSelection(
TextSelection(baseOffset: range.start, extentOffset: range.end),
......@@ -3217,7 +3217,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
/// * [TextPainter.getPositionForOffset], which is the equivalent method
/// for a [TextPainter] object.
TextPosition getPositionForPoint(Offset globalPosition) {
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
_computeTextMetricsIfNeeded();
globalPosition += -_paintOffset;
return _textPainter.getPositionForOffset(globalToLocal(globalPosition));
}
......@@ -3234,7 +3234,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
/// * [TextPainter.getOffsetForCaret], the equivalent method for a
/// [TextPainter] object.
Rect getLocalRectForCaret(TextPosition caretPosition) {
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
_computeTextMetricsIfNeeded();
final Offset caretOffset = _textPainter.getOffsetForCaret(caretPosition, _caretPrototype);
// This rect is the same as _caretPrototype but without the vertical padding.
final Rect rect = Rect.fromLTWH(0.0, 0.0, cursorWidth, cursorHeight).shift(caretOffset + _paintOffset + cursorOffset);
......@@ -3306,7 +3306,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
@override
double computeDistanceToActualBaseline(TextBaseline baseline) {
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
_computeTextMetricsIfNeeded();
return _textPainter.computeDistanceToActualBaseline(baseline);
}
......@@ -3500,7 +3500,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
void selectWordsInRange({ required Offset from, Offset? to, required SelectionChangedCause cause }) {
assert(cause != null);
assert(from != null);
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
_computeTextMetricsIfNeeded();
final TextPosition firstPosition = _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset));
final TextSelection firstWord = _getWordAtOffset(firstPosition);
final TextSelection lastWord = to == null ?
......@@ -3521,7 +3521,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
/// {@macro flutter.rendering.RenderEditable.selectPosition}
void selectWordEdge({ required SelectionChangedCause cause }) {
assert(cause != null);
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
_computeTextMetricsIfNeeded();
assert(_lastTapDownPosition != null);
final TextPosition position = _textPainter.getPositionForOffset(globalToLocal(_lastTapDownPosition! - _paintOffset));
final TextRange word = _textPainter.getWordBoundary(position);
......@@ -3693,8 +3693,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
void _layoutText({ double minWidth = 0.0, double maxWidth = double.infinity }) {
assert(maxWidth != null && minWidth != null);
if (_textLayoutLastMaxWidth == maxWidth && _textLayoutLastMinWidth == minWidth)
return;
final double availableMaxWidth = math.max(0.0, maxWidth - _caretMargin);
final double availableMinWidth = math.min(minWidth, availableMaxWidth);
final double textMaxWidth = _isMultiline ? availableMaxWidth : double.infinity;
......@@ -3707,6 +3705,30 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
_textLayoutLastMaxWidth = maxWidth;
}
// Computes the text metrics if `_textPainter`'s layout information was marked
// as dirty.
//
// This method must be called in `RenderEditable`'s public methods that expose
// `_textPainter`'s metrics. For instance, `systemFontsDidChange` sets
// _textPainter._paragraph to null, so accessing _textPainter's metrics
// immediately after `systemFontsDidChange` without first calling this method
// may crash.
//
// This method is also called in various paint methods (`RenderEditable.paint`
// as well as its foreground/background painters' `paint`). It's needed
// because invisible render objects kept in the tree by `KeepAlive` may not
// get a chance to do layout but can still paint.
// See https://github.com/flutter/flutter/issues/84896.
//
// This method only re-computes layout if the underlying `_textPainter`'s
// layout cache is invalidated (by calling `TextPainter.markNeedsLayout`), or
// the constraints used to layout the `_textPainter` is different. See
// `TextPainter.layout`.
void _computeTextMetricsIfNeeded() {
assert(constraints != null);
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
}
late Rect _caretPrototype;
// TODO(garyq): This is no longer producing the highest-fidelity caret
......@@ -3790,7 +3812,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
final BoxConstraints constraints = this.constraints;
_placeholderDimensions = _layoutChildren(constraints);
_textPainter.setPlaceholderDimensions(_placeholderDimensions);
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
_computeTextMetricsIfNeeded();
_setParentData();
_computeCaretPrototype();
// We grab _textPainter.size here because assigning to `size` on the next
......@@ -3983,7 +4005,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
@override
void paint(PaintingContext context, Offset offset) {
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
_computeTextMetricsIfNeeded();
if (_hasVisualOverflow && clipBehavior != Clip.none) {
_clipRectLayer = context.pushClipRect(
needsCompositing,
......@@ -4071,6 +4093,7 @@ class _RenderEditableCustomPaint extends RenderBox {
assert(parent != null);
final RenderEditablePainter? painter = this.painter;
if (painter != null && parent != null) {
parent._computeTextMetricsIfNeeded();
painter.paint(context.canvas, size, parent);
}
}
......
......@@ -3928,6 +3928,37 @@ void main() {
expect(result.path, hasLength(0));
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020
});
test('does not skip TextPainter.layout because of invalid cache', () {
// Regression test for https://github.com/flutter/flutter/issues/84896.
final TextSelectionDelegate delegate = FakeEditableTextState();
const BoxConstraints constraints = BoxConstraints(minWidth: 100, maxWidth: 500);
final RenderEditable editable = RenderEditable(
text: const TextSpan(
style: TextStyle(height: 1.0, fontSize: 10.0, fontFamily: 'Ahem'),
text: 'A',
),
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
textAlign: TextAlign.start,
textDirection: TextDirection.ltr,
locale: const Locale('en', 'US'),
forceLine: true,
offset: ViewportOffset.fixed(10.0),
textSelectionDelegate: delegate,
selection: const TextSelection.collapsed(offset: 0),
cursorColor: const Color(0xFFFFFFFF),
showCursor: ValueNotifier<bool>(true),
);
layout(editable, constraints: constraints);
final double initialWidth = editable.computeDryLayout(constraints).width;
expect(initialWidth, 500);
// Turn off forceLine. Now the width should be significantly smaller.
editable.forceLine = false;
expect(editable.computeDryLayout(constraints).width, lessThan(initialWidth));
});
}
class _TestRenderEditable extends RenderEditable {
......
......@@ -1142,6 +1142,76 @@ void main() {
expect(state.wantKeepAlive, true);
});
testWidgets(
'kept-alive EditableText does not crash when layout is skipped',
(WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/84896.
EditableText.debugDeterministicCursor = true;
const Key key = ValueKey<String>('EditableText');
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(),
child: Directionality(
textDirection: TextDirection.ltr,
child: ListView(
children: <Widget>[
EditableText(
key: key,
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
autofocus: true,
maxLines: null,
keyboardType: TextInputType.text,
style: textStyle,
textAlign: TextAlign.left,
cursorColor: cursorColor,
showCursor: false,
),
],
),
),
),
);
// Wait for autofocus.
await tester.pump();
expect(focusNode.hasFocus, isTrue);
// Prepend an additional item to make EditableText invisible. It's still
// kept in the tree via the keepalive mechanism. Change the text alignment
// and showCursor. The RenderEditable now needs to relayout and repaint.
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(),
child: Directionality(
textDirection: TextDirection.ltr,
child: ListView(
children: <Widget>[
const SizedBox(height: 6000),
EditableText(
key: key,
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
autofocus: true,
maxLines: null,
keyboardType: TextInputType.text,
style: textStyle,
textAlign: TextAlign.right,
cursorColor: cursorColor,
showCursor: true,
),
],
),
),
),
);
EditableText.debugDeterministicCursor = false;
expect(tester.takeException(), isNull);
});
/// Toolbar is not used in Flutter Web. Skip this check.
///
/// Web is using native DOM elements (it is also used as platform input)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment