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 {
/// Valid only after [layout] has been called.
List<ui.LineMetrics> computeLineMetrics() {
assert(_debugAssertTextLayoutIsValid);
return _lineMetricsCache ??= _paragraph!.computeLineMetrics();
return _lineMetricsCache ??= _paragraph!.computeLineMetrics();
}
bool _disposed = false;
......
......@@ -318,6 +318,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
textDirection: textDirection,
textScaleFactor: textScaleFactor,
locale: locale,
maxLines: maxLines == 1 ? 1 : null,
strutStyle: strutStyle,
textHeightBehavior: textHeightBehavior,
textWidthBasis: textWidthBasis,
......@@ -781,8 +782,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
// Returns the obscured text when [obscureText] is true. See
// [obscureText] and [obscuringCharacter].
String get _plainText {
_cachedPlainText ??= _textPainter.text!.toPlainText(includeSemanticsLabels: false);
return _cachedPlainText!;
return _cachedPlainText ??= _textPainter.text!.toPlainText(includeSemanticsLabels: false);
}
/// The text to display.
......@@ -794,8 +794,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
if (_textPainter.text == value) {
return;
}
_textPainter.text = value;
_cachedPlainText = null;
_cachedLineBreakCount = null;
_textPainter.text = value;
_cachedAttributedValue = null;
_cachedCombinedSemanticsInfos = null;
_extractPlaceholderSpans(value);
......@@ -965,6 +967,11 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return;
}
_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();
}
......@@ -1790,42 +1797,72 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
/// This does not require the layout to be updated.
double get preferredLineHeight => _textPainter.preferredLineHeight;
double _preferredHeight(double width) {
// Lock height to maxLines if needed.
final bool lockedMax = maxLines != null && minLines == null;
final bool lockedBoth = minLines != null && minLines == maxLines;
final bool singleLine = maxLines == 1;
if (singleLine || lockedMax || lockedBoth) {
return preferredLineHeight * maxLines!;
}
// Clamp height to minLines or maxLines if needed.
final bool minLimited = minLines != null && minLines! > 1;
final bool maxLimited = maxLines != null;
if (minLimited || maxLimited) {
_layoutText(maxWidth: width);
if (minLimited && _textPainter.height < preferredLineHeight * minLines!) {
return preferredLineHeight * minLines!;
}
if (maxLimited && _textPainter.height > preferredLineHeight * maxLines!) {
return preferredLineHeight * maxLines!;
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) {
return cachedValue;
}
int count = 0;
for (int index = 0; index < text.length; index += 1) {
switch (text.codeUnitAt(index)) {
case 0x000A: // LF
case 0x0085: // NEL
case 0x000B: // VT
case 0x000C: // FF, treating it as a regular line separator
case 0x2028: // LS
case 0x2029: // PS
count += 1;
}
}
return _cachedLineBreakCount = count;
}
// Set the height based on the content.
if (width == double.infinity) {
final String text = _plainText;
int lines = 1;
for (int index = 0; index < text.length; index += 1) {
// Count explicit line breaks.
if (text.codeUnitAt(index) == 0x0A) {
lines += 1;
}
double _preferredHeight(double width) {
final int? maxLines = this.maxLines;
final int? minLines = this.minLines ?? maxLines;
final double minHeight = preferredLineHeight * (minLines ?? 0);
if (maxLines == null) {
final double estimatedHeight;
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);
return math.max(preferredLineHeight, _textPainter.height);
final double maxHeight = preferredLineHeight * maxLines;
return clampDouble(_textPainter.height, minHeight, maxHeight);
}
@override
......@@ -1852,14 +1889,17 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
// Hit test text spans.
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
RenderBox? child = firstChild;
int childIndex = 0;
......@@ -2359,7 +2399,8 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
final Size textPainterSize = _textPainter.size;
final double width = forceLine ? constraints.maxWidth : constraints
.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 BoxConstraints painterConstraints = BoxConstraints.tight(contentSize);
......@@ -2595,8 +2636,9 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
_clipRectLayer.layer = null;
_paintContents(context, offset);
}
if (selection!.isValid) {
_paintHandleLayers(context, getEndpointsForSelection(selection!), offset);
final TextSelection? selection = this.selection;
if (selection != null && selection.isValid) {
_paintHandleLayers(context, getEndpointsForSelection(selection), offset);
}
}
......
......@@ -31,43 +31,32 @@ abstract class RenderShiftedBox extends RenderBox with RenderObjectWithChildMixi
@override
double computeMinIntrinsicWidth(double height) {
if (child != null) {
return child!.getMinIntrinsicWidth(height);
}
return 0.0;
return child?.getMinIntrinsicWidth(height) ?? 0.0;
}
@override
double computeMaxIntrinsicWidth(double height) {
if (child != null) {
return child!.getMaxIntrinsicWidth(height);
}
return 0.0;
return child?.getMaxIntrinsicWidth(height) ?? 0.0;
}
@override
double computeMinIntrinsicHeight(double width) {
if (child != null) {
return child!.getMinIntrinsicHeight(width);
}
return 0.0;
return child?.getMinIntrinsicHeight(width) ?? 0.0;
}
@override
double computeMaxIntrinsicHeight(double width) {
if (child != null) {
return child!.getMaxIntrinsicHeight(width);
}
return 0.0;
return child?.getMaxIntrinsicHeight(width) ?? 0.0;
}
@override
double? computeDistanceToActualBaseline(TextBaseline baseline) {
double? result;
final RenderBox? child = this.child;
if (child != null) {
assert(!debugNeedsLayout);
result = child!.getDistanceToActualBaseline(baseline);
final BoxParentData childParentData = child!.parentData! as BoxParentData;
result = child.getDistanceToActualBaseline(baseline);
final BoxParentData childParentData = child.parentData! as BoxParentData;
if (result != null) {
result += childParentData.offset.dy;
}
......@@ -79,22 +68,24 @@ abstract class RenderShiftedBox extends RenderBox with RenderObjectWithChildMixi
@override
void paint(PaintingContext context, Offset offset) {
final RenderBox? child = this.child;
if (child != null) {
final BoxParentData childParentData = child!.parentData! as BoxParentData;
context.paintChild(child!, childParentData.offset + offset);
final BoxParentData childParentData = child.parentData! as BoxParentData;
context.paintChild(child, childParentData.offset + offset);
}
}
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
final RenderBox? child = this.child;
if (child != null) {
final BoxParentData childParentData = child!.parentData! as BoxParentData;
final BoxParentData childParentData = child.parentData! as BoxParentData;
return result.addWithPaintOffset(
offset: childParentData.offset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
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 {
/// [TextEditingController.addListener].
///
/// [onChanged] is called before [onSubmitted] when user indicates completion
/// of editing, such as when pressing the "done" button on the keyboard. That default
/// behavior can be overridden. See [onEditingComplete] for details.
/// of editing, such as when pressing the "done" button on the keyboard. That
/// default behavior can be overridden. See [onEditingComplete] for details.
///
/// {@tool dartpad}
/// This example shows how onChanged could be used to check the TextField's
......
......@@ -5149,9 +5149,13 @@ void main() {
height: 200.0,
width: 200.0,
child: Center(
child: CupertinoTextField(
controller: OverflowWidgetTextEditingController(),
clipBehavior: Clip.none,
child: SizedBox(
// Make sure the input field is not high enough for the WidgetSpan.
height: 50,
child: CupertinoTextField(
controller: OverflowWidgetTextEditingController(),
clipBehavior: Clip.none,
),
),
),
),
......
......@@ -841,9 +841,13 @@ void main() {
height: 200,
width: 200,
child: Center(
child: TextField(
controller: OverflowWidgetTextEditingController(),
clipBehavior: Clip.none,
child: SizedBox(
// Make sure the input field is not high enough for the WidgetSpan.
height: 50,
child: TextField(
controller: OverflowWidgetTextEditingController(),
clipBehavior: Clip.none,
),
),
),
),
......@@ -9068,6 +9072,7 @@ void main() {
home: Material(
child: Center(
child: TextField(
maxLines: null,
controller: controller,
),
),
......
......@@ -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', () {
final String longString = 'a' * 10000;
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