Unverified Commit 78e94155 authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

[`RenderEditable`] report real height when `maxLines == 1`. (#112029)

parent 6e02485b
...@@ -1154,7 +1154,7 @@ class TextPainter { ...@@ -1154,7 +1154,7 @@ class TextPainter {
/// Valid only after [layout] has been called. /// Valid only after [layout] has been called.
List<ui.LineMetrics> computeLineMetrics() { List<ui.LineMetrics> computeLineMetrics() {
assert(_debugAssertTextLayoutIsValid); assert(_debugAssertTextLayoutIsValid);
return _lineMetricsCache ??= _paragraph!.computeLineMetrics(); return _lineMetricsCache ??= _paragraph!.computeLineMetrics();
} }
bool _disposed = false; bool _disposed = false;
......
...@@ -318,6 +318,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -318,6 +318,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
textDirection: textDirection, textDirection: textDirection,
textScaleFactor: textScaleFactor, textScaleFactor: textScaleFactor,
locale: locale, locale: locale,
maxLines: maxLines == 1 ? 1 : null,
strutStyle: strutStyle, strutStyle: strutStyle,
textHeightBehavior: textHeightBehavior, textHeightBehavior: textHeightBehavior,
textWidthBasis: textWidthBasis, textWidthBasis: textWidthBasis,
...@@ -781,8 +782,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -781,8 +782,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
// Returns the obscured text when [obscureText] is true. See // Returns the obscured text when [obscureText] is true. See
// [obscureText] and [obscuringCharacter]. // [obscureText] and [obscuringCharacter].
String get _plainText { String get _plainText {
_cachedPlainText ??= _textPainter.text!.toPlainText(includeSemanticsLabels: false); return _cachedPlainText ??= _textPainter.text!.toPlainText(includeSemanticsLabels: false);
return _cachedPlainText!;
} }
/// The text to display. /// The text to display.
...@@ -794,8 +794,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -794,8 +794,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
if (_textPainter.text == value) { if (_textPainter.text == value) {
return; return;
} }
_textPainter.text = value;
_cachedPlainText = null; _cachedPlainText = null;
_cachedLineBreakCount = null;
_textPainter.text = value;
_cachedAttributedValue = null; _cachedAttributedValue = null;
_cachedCombinedSemanticsInfos = null; _cachedCombinedSemanticsInfos = null;
_extractPlaceholderSpans(value); _extractPlaceholderSpans(value);
...@@ -965,6 +967,11 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -965,6 +967,11 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return; return;
} }
_maxLines = value; _maxLines = value;
// Special case maxLines == 1 to keep only the first line so we can get the
// 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(); markNeedsTextLayout();
} }
...@@ -1790,42 +1797,72 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -1790,42 +1797,72 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
/// This does not require the layout to be updated. /// This does not require the layout to be updated.
double get preferredLineHeight => _textPainter.preferredLineHeight; double get preferredLineHeight => _textPainter.preferredLineHeight;
double _preferredHeight(double width) { int? _cachedLineBreakCount;
// Lock height to maxLines if needed. // TODO(LongCatIsLooong): see if we can let ui.Paragraph estimate the number
final bool lockedMax = maxLines != null && minLines == null; // of lines
final bool lockedBoth = minLines != null && minLines == maxLines; int _countHardLineBreaks(String text) {
final bool singleLine = maxLines == 1; final int? cachedValue = _cachedLineBreakCount;
if (singleLine || lockedMax || lockedBoth) { if (cachedValue != null) {
return preferredLineHeight * maxLines!; return cachedValue;
} }
int count = 0;
// Clamp height to minLines or maxLines if needed. for (int index = 0; index < text.length; index += 1) {
final bool minLimited = minLines != null && minLines! > 1; switch (text.codeUnitAt(index)) {
final bool maxLimited = maxLines != null; case 0x000A: // LF
if (minLimited || maxLimited) { case 0x0085: // NEL
_layoutText(maxWidth: width); case 0x000B: // VT
if (minLimited && _textPainter.height < preferredLineHeight * minLines!) { case 0x000C: // FF, treating it as a regular line separator
return preferredLineHeight * minLines!; case 0x2028: // LS
} case 0x2029: // PS
if (maxLimited && _textPainter.height > preferredLineHeight * maxLines!) { count += 1;
return preferredLineHeight * maxLines!;
} }
} }
return _cachedLineBreakCount = count;
}
// Set the height based on the content. double _preferredHeight(double width) {
if (width == double.infinity) { final int? maxLines = this.maxLines;
final String text = _plainText; final int? minLines = this.minLines ?? maxLines;
int lines = 1; final double minHeight = preferredLineHeight * (minLines ?? 0);
for (int index = 0; index < text.length; index += 1) {
// Count explicit line breaks. if (maxLines == null) {
if (text.codeUnitAt(index) == 0x0A) { final double estimatedHeight;
lines += 1; if (width == double.infinity) {
} estimatedHeight = preferredLineHeight * (_countHardLineBreaks(_plainText) + 1);
} else {
_layoutText(maxWidth: width);
estimatedHeight = _textPainter.height;
} }
return preferredLineHeight * lines; return math.max(estimatedHeight, minHeight);
}
// TODO(LongCatIsLooong): this is a workaround for
// https://github.com/flutter/flutter/issues/112123 .
// Use preferredLineHeight since SkParagraph currently returns an incorrect
// height.
final TextHeightBehavior? textHeightBehavior = this.textHeightBehavior;
final bool usePreferredLineHeightHack = maxLines == 1
&& text?.codeUnitAt(0) == null
&& strutStyle != null && strutStyle != StrutStyle.disabled
&& textHeightBehavior != null
&& (!textHeightBehavior.applyHeightToFirstAscent || !textHeightBehavior.applyHeightToLastDescent);
// Special case maxLines == 1 since it forces the scrollable direction
// to be horizontal. Report the real height to prevent the text from being
// clipped.
if (maxLines == 1 && !usePreferredLineHeightHack) {
// 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;
}
if (minLines == maxLines) {
return minHeight;
} }
_layoutText(maxWidth: width); _layoutText(maxWidth: width);
return math.max(preferredLineHeight, _textPainter.height); final double maxHeight = preferredLineHeight * maxLines;
return clampDouble(_textPainter.height, minHeight, maxHeight);
} }
@override @override
...@@ -1852,14 +1889,17 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -1852,14 +1889,17 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
// Hit test text spans. // Hit test text spans.
bool hitText = false; bool hitText = false;
final Offset effectivePosition = position - _paintOffset;
final TextPosition textPosition = _textPainter.getPositionForOffset(effectivePosition);
final InlineSpan? span = _textPainter.text!.getSpanForPosition(textPosition);
if (span != null && span is HitTestTarget) {
result.add(HitTestEntry(span as HitTestTarget));
hitText = true;
}
final InlineSpan? textSpan = _textPainter.text;
if (textSpan != null) {
final Offset effectivePosition = position - _paintOffset;
final TextPosition textPosition = _textPainter.getPositionForOffset(effectivePosition);
final Object? span = textSpan.getSpanForPosition(textPosition);
if (span is HitTestTarget) {
result.add(HitTestEntry(span));
hitText = true;
}
}
// Hit test render object children // Hit test render object children
RenderBox? child = firstChild; RenderBox? child = firstChild;
int childIndex = 0; int childIndex = 0;
...@@ -2359,7 +2399,8 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -2359,7 +2399,8 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
final Size textPainterSize = _textPainter.size; final Size textPainterSize = _textPainter.size;
final double width = forceLine ? constraints.maxWidth : constraints final double width = forceLine ? constraints.maxWidth : constraints
.constrainWidth(_textPainter.size.width + _caretMargin); .constrainWidth(_textPainter.size.width + _caretMargin);
size = Size(width, constraints.constrainHeight(_preferredHeight(constraints.maxWidth))); final double preferredHeight = _preferredHeight(constraints.maxWidth);
size = Size(width, constraints.constrainHeight(preferredHeight));
final Size contentSize = Size(textPainterSize.width + _caretMargin, textPainterSize.height); final Size contentSize = Size(textPainterSize.width + _caretMargin, textPainterSize.height);
final BoxConstraints painterConstraints = BoxConstraints.tight(contentSize); final BoxConstraints painterConstraints = BoxConstraints.tight(contentSize);
...@@ -2595,8 +2636,9 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -2595,8 +2636,9 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
_clipRectLayer.layer = null; _clipRectLayer.layer = null;
_paintContents(context, offset); _paintContents(context, offset);
} }
if (selection!.isValid) { final TextSelection? selection = this.selection;
_paintHandleLayers(context, getEndpointsForSelection(selection!), offset); if (selection != null && selection.isValid) {
_paintHandleLayers(context, getEndpointsForSelection(selection), offset);
} }
} }
......
...@@ -31,43 +31,32 @@ abstract class RenderShiftedBox extends RenderBox with RenderObjectWithChildMixi ...@@ -31,43 +31,32 @@ abstract class RenderShiftedBox extends RenderBox with RenderObjectWithChildMixi
@override @override
double computeMinIntrinsicWidth(double height) { double computeMinIntrinsicWidth(double height) {
if (child != null) { return child?.getMinIntrinsicWidth(height) ?? 0.0;
return child!.getMinIntrinsicWidth(height);
}
return 0.0;
} }
@override @override
double computeMaxIntrinsicWidth(double height) { double computeMaxIntrinsicWidth(double height) {
if (child != null) { return child?.getMaxIntrinsicWidth(height) ?? 0.0;
return child!.getMaxIntrinsicWidth(height);
}
return 0.0;
} }
@override @override
double computeMinIntrinsicHeight(double width) { double computeMinIntrinsicHeight(double width) {
if (child != null) { return child?.getMinIntrinsicHeight(width) ?? 0.0;
return child!.getMinIntrinsicHeight(width);
}
return 0.0;
} }
@override @override
double computeMaxIntrinsicHeight(double width) { double computeMaxIntrinsicHeight(double width) {
if (child != null) { return child?.getMaxIntrinsicHeight(width) ?? 0.0;
return child!.getMaxIntrinsicHeight(width);
}
return 0.0;
} }
@override @override
double? computeDistanceToActualBaseline(TextBaseline baseline) { double? computeDistanceToActualBaseline(TextBaseline baseline) {
double? result; double? result;
final RenderBox? child = this.child;
if (child != null) { if (child != null) {
assert(!debugNeedsLayout); assert(!debugNeedsLayout);
result = child!.getDistanceToActualBaseline(baseline); result = child.getDistanceToActualBaseline(baseline);
final BoxParentData childParentData = child!.parentData! as BoxParentData; final BoxParentData childParentData = child.parentData! as BoxParentData;
if (result != null) { if (result != null) {
result += childParentData.offset.dy; result += childParentData.offset.dy;
} }
...@@ -79,22 +68,24 @@ abstract class RenderShiftedBox extends RenderBox with RenderObjectWithChildMixi ...@@ -79,22 +68,24 @@ abstract class RenderShiftedBox extends RenderBox with RenderObjectWithChildMixi
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
final RenderBox? child = this.child;
if (child != null) { if (child != null) {
final BoxParentData childParentData = child!.parentData! as BoxParentData; final BoxParentData childParentData = child.parentData! as BoxParentData;
context.paintChild(child!, childParentData.offset + offset); context.paintChild(child, childParentData.offset + offset);
} }
} }
@override @override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
final RenderBox? child = this.child;
if (child != null) { if (child != null) {
final BoxParentData childParentData = child!.parentData! as BoxParentData; final BoxParentData childParentData = child.parentData! as BoxParentData;
return result.addWithPaintOffset( return result.addWithPaintOffset(
offset: childParentData.offset, offset: childParentData.offset,
position: position, position: position,
hitTest: (BoxHitTestResult result, Offset transformed) { hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - childParentData.offset); assert(transformed == position - childParentData.offset);
return child!.hitTest(result, position: transformed); return child.hitTest(result, position: transformed);
}, },
); );
} }
......
...@@ -1139,8 +1139,8 @@ class EditableText extends StatefulWidget { ...@@ -1139,8 +1139,8 @@ class EditableText extends StatefulWidget {
/// [TextEditingController.addListener]. /// [TextEditingController.addListener].
/// ///
/// [onChanged] is called before [onSubmitted] when user indicates completion /// [onChanged] is called before [onSubmitted] when user indicates completion
/// of editing, such as when pressing the "done" button on the keyboard. That default /// of editing, such as when pressing the "done" button on the keyboard. That
/// behavior can be overridden. See [onEditingComplete] for details. /// default behavior can be overridden. See [onEditingComplete] for details.
/// ///
/// {@tool dartpad} /// {@tool dartpad}
/// This example shows how onChanged could be used to check the TextField's /// This example shows how onChanged could be used to check the TextField's
......
...@@ -5149,9 +5149,13 @@ void main() { ...@@ -5149,9 +5149,13 @@ void main() {
height: 200.0, height: 200.0,
width: 200.0, width: 200.0,
child: Center( child: Center(
child: CupertinoTextField( child: SizedBox(
controller: OverflowWidgetTextEditingController(), // Make sure the input field is not high enough for the WidgetSpan.
clipBehavior: Clip.none, height: 50,
child: CupertinoTextField(
controller: OverflowWidgetTextEditingController(),
clipBehavior: Clip.none,
),
), ),
), ),
), ),
......
...@@ -841,9 +841,13 @@ void main() { ...@@ -841,9 +841,13 @@ void main() {
height: 200, height: 200,
width: 200, width: 200,
child: Center( child: Center(
child: TextField( child: SizedBox(
controller: OverflowWidgetTextEditingController(), // Make sure the input field is not high enough for the WidgetSpan.
clipBehavior: Clip.none, height: 50,
child: TextField(
controller: OverflowWidgetTextEditingController(),
clipBehavior: Clip.none,
),
), ),
), ),
), ),
...@@ -9068,6 +9072,7 @@ void main() { ...@@ -9068,6 +9072,7 @@ void main() {
home: Material( home: Material(
child: Center( child: Center(
child: TextField( child: TextField(
maxLines: null,
controller: controller, controller: controller,
), ),
), ),
......
...@@ -95,6 +95,44 @@ void main() { ...@@ -95,6 +95,44 @@ void main() {
} }
}); });
test('Reports the real height when maxLines is 1', () {
const InlineSpan tallSpan = TextSpan(
style: TextStyle(fontSize: 10),
children: <InlineSpan>[TextSpan(text: 'TALL', style: TextStyle(fontSize: 100))],
);
final BoxConstraints constraints = BoxConstraints.loose(const Size(600, 600));
final RenderEditable editable = RenderEditable(
textDirection: TextDirection.ltr,
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
offset: ViewportOffset.zero(),
textSelectionDelegate: _FakeEditableTextState(),
text: tallSpan,
);
layout(editable, constraints: constraints);
expect(editable.size.height, 100);
});
test('Reports the height of the first line when maxLines is 1', () {
final InlineSpan multilineSpan = TextSpan(
text: 'liiiiines\n' * 10,
style: const TextStyle(fontSize: 10),
);
final BoxConstraints constraints = BoxConstraints.loose(const Size(600, 600));
final RenderEditable editable = RenderEditable(
textDirection: TextDirection.ltr,
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
offset: ViewportOffset.zero(),
textSelectionDelegate: _FakeEditableTextState(),
text: multilineSpan,
);
layout(editable, constraints: constraints);
expect(editable.size.height, 10);
});
test('Editable respect clipBehavior in describeApproximatePaintClip', () { test('Editable respect clipBehavior in describeApproximatePaintClip', () {
final String longString = 'a' * 10000; final String longString = 'a' * 10000;
final RenderEditable editable = RenderEditable( final RenderEditable editable = RenderEditable(
......
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