From d74a5883d9f01db0b5cda7505639b540cd37a780 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Mon, 12 Jun 2017 21:47:31 -0700 Subject: [PATCH] Allow multi-line text fields with no line limit (#10576) --- .../flutter/lib/src/material/text_field.dart | 21 ++++++- .../lib/src/material/text_form_field.dart | 9 ++- .../lib/src/painting/text_painter.dart | 5 ++ .../flutter/lib/src/painting/text_style.dart | 8 ++- .../flutter/lib/src/rendering/editable.dart | 63 ++++++++++++++----- .../flutter/lib/src/rendering/paragraph.dart | 20 ++++-- packages/flutter/lib/src/widgets/basic.dart | 10 ++- .../lib/src/widgets/editable_text.dart | 18 ++++-- packages/flutter/lib/src/widgets/text.dart | 36 ++++++++++- .../test/material/text_field_test.dart | 48 +++++++++----- .../test/rendering/paragraph_test.dart | 34 +++++++++- 11 files changed, 219 insertions(+), 53 deletions(-) diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index e9a7bc8090..46fb6fe34c 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart @@ -62,6 +62,12 @@ class TextField extends StatefulWidget { /// To remove the decoration entirely (including the extra padding introduced /// by the decoration to save space for the labels), set the [decoration] to /// null. + /// + /// The [maxLines] property can be set to null to remove the restriction on + /// the number of lines. By default, it is 1, meaning this is a single-line + /// text field. If it is not null, it must be greater than zero. + /// + /// The [keyboardType], [autofocus], and [obscureText] arguments must not be null. const TextField({ Key key, this.controller, @@ -76,7 +82,11 @@ class TextField extends StatefulWidget { this.onChanged, this.onSubmitted, this.inputFormatters, - }) : super(key: key); + }) : assert(keyboardType != null), + assert(autofocus != null), + assert(obscureText != null), + assert(maxLines == null || maxLines > 0), + super(key: key); /// Controls the text being edited. /// @@ -98,6 +108,8 @@ class TextField extends StatefulWidget { final InputDecoration decoration; /// The type of keyboard to use for editing the text. + /// + /// Defaults to [TextInputType.text]. Cannot be null. final TextInputType keyboardType; /// The style to use for the text being edited. @@ -116,7 +128,7 @@ class TextField extends StatefulWidget { /// If true, the keyboard will open as soon as this text field obtains focus. /// Otherwise, the keyboard is only shown after the user taps the text field. /// - /// Defaults to false. + /// Defaults to false. Cannot be null. // See https://github.com/flutter/flutter/issues/7035 for the rationale for this // keyboard behavior. final bool autofocus; @@ -126,13 +138,16 @@ class TextField extends StatefulWidget { /// When this is set to true, all the characters in the text field are /// replaced by U+2022 BULLET characters (•). /// - /// Defaults to false. + /// Defaults to false. Cannot be null. final bool obscureText; /// The maximum number of lines for the text to span, wrapping if necessary. /// /// If this is 1 (the default), the text will not wrap, but will scroll /// horizontally instead. + /// + /// If this is null, there is no limit to the number of lines. If it is not + /// null, the value must be greater than zero. final int maxLines; /// Called when the text being edited changes. diff --git a/packages/flutter/lib/src/material/text_form_field.dart b/packages/flutter/lib/src/material/text_form_field.dart index 8ef46d915f..49726958c6 100644 --- a/packages/flutter/lib/src/material/text_form_field.dart +++ b/packages/flutter/lib/src/material/text_form_field.dart @@ -30,7 +30,8 @@ import 'text_field.dart'; class TextFormField extends FormField { /// Creates a [FormField] that contains a [TextField]. /// - /// For a documentation about the various parameters, see [TextField]. + /// For documentation about the various parameters, see the [TextField] class + /// and [new TextField], the constructor. TextFormField({ Key key, TextEditingController controller, @@ -44,7 +45,11 @@ class TextFormField extends FormField { FormFieldSetter onSaved, FormFieldValidator validator, List inputFormatters, - }) : super( + }) : assert(keyboardType != null), + assert(autofocus != null), + assert(obscureText != null), + assert(maxLines == null || maxLines > 0), + super( key: key, initialValue: controller != null ? controller.value.text : '', onSaved: onSaved, diff --git a/packages/flutter/lib/src/painting/text_painter.dart b/packages/flutter/lib/src/painting/text_painter.dart index 8afa2858e7..03a3ca9355 100644 --- a/packages/flutter/lib/src/painting/text_painter.dart +++ b/packages/flutter/lib/src/painting/text_painter.dart @@ -35,6 +35,8 @@ class TextPainter { /// /// The text argument is optional but [text] must be non-null before calling /// [layout]. + /// + /// The [maxLines] property, if non-null, must be greater than zero. TextPainter({ TextSpan text, TextAlign textAlign, @@ -43,6 +45,7 @@ class TextPainter { String ellipsis, }) : assert(text == null || text.debugAssertIsValid()), assert(textScaleFactor != null), + assert(maxLines == null || maxLines > 0), _text = text, _textAlign = textAlign, _textScaleFactor = textScaleFactor, @@ -134,7 +137,9 @@ class TextPainter { /// After this is set, you must call [layout] before the next call to [paint]. int get maxLines => _maxLines; int _maxLines; + /// The value may be null. If it is not null, then it must be greater than zero. set maxLines(int value) { + assert(value == null || value > 0); if (_maxLines == value) return; _maxLines = value; diff --git a/packages/flutter/lib/src/painting/text_style.dart b/packages/flutter/lib/src/painting/text_style.dart index 5fcabe8d7b..d56f82b3cf 100644 --- a/packages/flutter/lib/src/painting/text_style.dart +++ b/packages/flutter/lib/src/painting/text_style.dart @@ -234,12 +234,18 @@ class TextStyle { } /// The style information for paragraphs, encoded for use by `dart:ui`. + /// + /// The `textScaleFactor` argument must not be null. If omitted, it defaults + /// to 1.0. The other arguments may be null. The `maxLines` argument, if + /// specified and non-null, must be greater than zero. ui.ParagraphStyle getParagraphStyle({ TextAlign textAlign, double textScaleFactor: 1.0, String ellipsis, int maxLines, - }) { + }) { + assert(textScaleFactor != null); + assert(maxLines == null || maxLines > 0); return new ui.ParagraphStyle( textAlign: textAlign, fontWeight: fontWeight, diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart index 8622994a44..cde9cc9829 100644 --- a/packages/flutter/lib/src/rendering/editable.dart +++ b/packages/flutter/lib/src/rendering/editable.dart @@ -84,6 +84,15 @@ class TextSelectionPoint { /// responsibility of higher layers and not handled by this object. class RenderEditable extends RenderBox { /// Creates a render object that implements the visual aspects of a text field. + /// + /// If [showCursor] is not specified, then it defaults to hiding the cursor. + /// + /// The [maxLines] property can be set to null to remove the restriction on + /// the number of lines. By default, it is 1, meaning this is a single-line + /// text field. If it is not null, it must be greater than zero. + /// + /// The [offset] is required and must not be null. You can use [new + /// ViewportOffset.zero] if you have no need for scrolling. RenderEditable({ TextSpan text, TextAlign textAlign, @@ -96,7 +105,7 @@ class RenderEditable extends RenderBox { @required ViewportOffset offset, this.onSelectionChanged, this.onCaretChanged, - }) : assert(maxLines != null), + }) : assert(maxLines == null || maxLines > 0), assert(textScaleFactor != null), assert(offset != null), _textPainter = new TextPainter(text: text, textAlign: textAlign, textScaleFactor: textScaleFactor), @@ -180,13 +189,21 @@ class RenderEditable extends RenderBox { } /// The maximum number of lines for the text to span, wrapping if necessary. + /// /// If this is 1 (the default), the text will not wrap, but will extend /// indefinitely instead. + /// + /// If this is null, there is no limit to the number of lines. + /// + /// When this is not null, the intrinsic height of the render object is the + /// height of one line of text multiplied by this value. In other words, this + /// also controls the height of the actual editing widget. int get maxLines => _maxLines; int _maxLines; + /// The value may be null. If it is not null, then it must be greater than zero. set maxLines(int value) { - assert(value != null); - if (_maxLines == value) + assert(value == null || value > 0); + if (maxLines == value) return; _maxLines = value; markNeedsTextLayout(); @@ -261,7 +278,7 @@ class RenderEditable extends RenderBox { super.detach(); } - bool get _isMultiline => maxLines > 1; + bool get _isMultiline => maxLines != 1; Axis get _viewportAxis => _isMultiline ? Axis.vertical : Axis.horizontal; @@ -359,14 +376,30 @@ class RenderEditable extends RenderBox { // This does not required the layout to be updated. double get _preferredLineHeight => _textPainter.preferredLineHeight; + double _preferredHeight(double width) { + if (maxLines != null) + return _preferredLineHeight * maxLines; + if (width == double.INFINITY) { + final String text = _textPainter.text.toPlainText(); + int lines = 1; + for (int index = 0; index < text.length; index += 1) { + if (text.codeUnitAt(index) == 0x0A) // count explicit line breaks + lines += 1; + } + return _preferredLineHeight * lines; + } + _layoutText(width); + return math.max(_preferredLineHeight, _textPainter.height); + } + @override double computeMinIntrinsicHeight(double width) { - return _preferredLineHeight; + return _preferredHeight(width); } @override double computeMaxIntrinsicHeight(double width) { - return _preferredLineHeight * maxLines; + return _preferredHeight(width); } @override @@ -434,7 +467,7 @@ class RenderEditable extends RenderBox { return; final double caretMargin = _kCaretGap + _kCaretWidth; final double availableWidth = math.max(0.0, constraintWidth - caretMargin); - final double maxWidth = _maxLines > 1 ? availableWidth : double.INFINITY; + final double maxWidth = _isMultiline ? availableWidth : double.INFINITY; _textPainter.layout(minWidth: availableWidth, maxWidth: maxWidth); _textLayoutLastWidth = constraintWidth; } @@ -444,9 +477,7 @@ class RenderEditable extends RenderBox { _layoutText(constraints.maxWidth); _caretPrototype = new Rect.fromLTWH(0.0, _kCaretHeightOffset, _kCaretWidth, _preferredLineHeight - 2.0 * _kCaretHeightOffset); _selectionRects = null; - size = new Size(constraints.maxWidth, constraints.constrainHeight( - _textPainter.height.clamp(_preferredLineHeight, _preferredLineHeight * _maxLines) - )); + size = new Size(constraints.maxWidth, constraints.constrainHeight(_preferredHeight(constraints.maxWidth))); final Size contentSize = new Size(_textPainter.width + _kCaretGap + _kCaretWidth, _textPainter.height); final double _maxScrollExtent = _getMaxScrollExtent(contentSize); _hasVisualOverflow = _maxScrollExtent > 0.0; @@ -506,13 +537,13 @@ class RenderEditable extends RenderBox { @override void debugFillDescription(List description) { super.debugFillDescription(description); - description.add('cursorColor: $_cursorColor'); - description.add('showCursor: $_showCursor'); - description.add('maxLines: $_maxLines'); - description.add('selectionColor: $_selectionColor'); + description.add('cursorColor: $cursorColor'); + description.add('showCursor: $showCursor'); + description.add('maxLines: $maxLines'); + description.add('selectionColor: $selectionColor'); description.add('textScaleFactor: $textScaleFactor'); - description.add('selection: $_selection'); - description.add('offset: $_offset'); + description.add('selection: $selection'); + description.add('offset: $offset'); } @override diff --git a/packages/flutter/lib/src/rendering/paragraph.dart b/packages/flutter/lib/src/rendering/paragraph.dart index 03d537005c..5262cdd4df 100644 --- a/packages/flutter/lib/src/rendering/paragraph.dart +++ b/packages/flutter/lib/src/rendering/paragraph.dart @@ -31,7 +31,11 @@ const String _kEllipsis = '\u2026'; class RenderParagraph extends RenderBox { /// Creates a paragraph render object. /// - /// The [text], [overflow], and [softWrap] arguments must not be null. + /// The [text], [overflow], [softWrap], and [textScaleFactor] arguments must + /// not be null. + /// + /// The [maxLines] property may be null (and indeed defaults to null), but if + /// it is not null, it must be greater than zero. RenderParagraph(TextSpan text, { TextAlign textAlign, bool softWrap: true, @@ -43,6 +47,7 @@ class RenderParagraph extends RenderBox { assert(softWrap != null), assert(overflow != null), assert(textScaleFactor != null), + assert(maxLines == null || maxLines > 0), _softWrap = softWrap, _overflow = overflow, _textPainter = new TextPainter( @@ -77,7 +82,11 @@ class RenderParagraph extends RenderBox { /// Whether the text should break at soft line breaks. /// - /// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space. + /// If false, the glyphs in the text will be positioned as if there was + /// unlimited horizontal space. + /// + /// If [softWrap] is false, [overflow] and [textAlign] may have unexpected + /// effects. bool get softWrap => _softWrap; bool _softWrap; set softWrap(bool value) { @@ -116,9 +125,11 @@ class RenderParagraph extends RenderBox { /// An optional maximum number of lines for the text to span, wrapping if necessary. /// If the text exceeds the given number of lines, it will be truncated according - /// to [overflow]. + /// to [overflow] and [softWrap]. int get maxLines => _textPainter.maxLines; + /// The value may be null. If it is not null, then it must be greater than zero. set maxLines(int value) { + assert(value == null || value > 0); if (_textPainter.maxLines == value) return; _textPainter.maxLines = value; @@ -127,8 +138,7 @@ class RenderParagraph extends RenderBox { } void _layoutText({ double minWidth: 0.0, double maxWidth: double.INFINITY }) { - final bool wrap = _softWrap || (_overflow == TextOverflow.ellipsis && maxLines == null); - _textPainter.layout(minWidth: minWidth, maxWidth: wrap ? maxWidth : double.INFINITY); + _textPainter.layout(minWidth: minWidth, maxWidth: _softWrap ? maxWidth : double.INFINITY); } void _layoutTextWithConstraints(BoxConstraints constraints) { diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 298824cf3d..9f0ed93431 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -3028,7 +3028,11 @@ class Flow extends MultiChildRenderObjectWidget { class RichText extends LeafRenderObjectWidget { /// Creates a paragraph of rich text. /// - /// The [text], [softWrap], and [overflow] arguments must not be null. + /// The [text], [softWrap], [overflow], nad [textScaleFactor] arguments must + /// not be null. + /// + /// The [maxLines] property may be null (and indeed defaults to null), but if + /// it is not null, it must be greater than zero. const RichText({ Key key, @required this.text, @@ -3041,6 +3045,7 @@ class RichText extends LeafRenderObjectWidget { assert(softWrap != null), assert(overflow != null), assert(textScaleFactor != null), + assert(maxLines == null || maxLines > 0), super(key: key); /// The text to display in this widget. @@ -3066,6 +3071,9 @@ class RichText extends LeafRenderObjectWidget { /// An optional maximum number of lines for the text to span, wrapping if necessary. /// If the text exceeds the given number of lines, it will be truncated according /// to [overflow]. + /// + /// If this is 1, text will not wrap. Otherwise, text will be wrapped at the + /// edge of the box. final int maxLines; @override diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index dddff7ef4b..a4c8e7d0a7 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -128,6 +128,10 @@ class TextEditingController extends ValueNotifier { class EditableText extends StatefulWidget { /// Creates a basic text input control. /// + /// The [maxLines] property can be set to null to remove the restriction on + /// the number of lines. By default, it is 1, meaning this is a single-line + /// text field. If it is not null, it must be greater than zero. + /// /// The [controller], [focusNode], [style], and [cursorColor] arguments must /// not be null. EditableText({ @@ -152,9 +156,9 @@ class EditableText extends StatefulWidget { assert(obscureText != null), assert(style != null), assert(cursorColor != null), - assert(maxLines != null), + assert(maxLines == null || maxLines > 0), assert(autofocus != null), - inputFormatters = maxLines == 1 + inputFormatters = maxLines == 1 ? ( [BlacklistingTextInputFormatter.singleLineFormatter] ..addAll(inputFormatters ?? const Iterable.empty()) @@ -192,8 +196,12 @@ class EditableText extends StatefulWidget { final Color cursorColor; /// The maximum number of lines for the text to span, wrapping if necessary. + /// /// If this is 1 (the default), the text will not wrap, but will scroll /// horizontally instead. + /// + /// If this is null, there is no limit to the number of lines. If it is not + /// null, the value must be greater than zero. final int maxLines; /// Whether this input field should focus itself if nothing else is already focused. @@ -218,7 +226,7 @@ class EditableText extends StatefulWidget { /// Called when the user indicates that they are done editing the text in the field. final ValueChanged onSubmitted; - /// Optional input validation and formatting overrides. Formatters are run + /// Optional input validation and formatting overrides. Formatters are run /// in the provided order when the text input changes. final List inputFormatters; @@ -340,7 +348,7 @@ class EditableTextState extends State implements TextInputClient { } bool get _hasFocus => widget.focusNode.hasFocus; - bool get _isMultiline => widget.maxLines > 1; + bool get _isMultiline => widget.maxLines != 1; // Calculate the new scroll offset so the cursor remains visible. double _getScrollOffsetForCaret(Rect caretRect) { @@ -417,7 +425,7 @@ class EditableTextState extends State implements TextInputClient { void _handleSelectionChanged(TextSelection selection, RenderEditable renderObject, bool longPress) { widget.controller.selection = selection; - // Note that this will show the keyboard for all selection changes on the + // This will show the keyboard for all selection changes on the // EditableWidget, not just changes triggered by user gestures. requestKeyboard(); diff --git a/packages/flutter/lib/src/widgets/text.dart b/packages/flutter/lib/src/widgets/text.dart index d56fa01579..a9b4313538 100644 --- a/packages/flutter/lib/src/widgets/text.dart +++ b/packages/flutter/lib/src/widgets/text.dart @@ -14,6 +14,14 @@ class DefaultTextStyle extends InheritedWidget { /// /// Consider using [DefaultTextStyle.merge] to inherit styling information /// from the current default text style for a given [BuildContext]. + /// + /// The [style] and [child] arguments are required and must not be null. + /// + /// The [softWrap] and [overflow] arguments must not be null (though they do + /// have default values). + /// + /// The [maxLines] property may be null (and indeed defaults to null), but if + /// it is not null, it must be greater than zero. const DefaultTextStyle({ Key key, @required this.style, @@ -25,6 +33,7 @@ class DefaultTextStyle extends InheritedWidget { }) : assert(style != null), assert(softWrap != null), assert(overflow != null), + assert(maxLines == null || maxLines > 0), assert(child != null), super(key: key, child: child); @@ -48,6 +57,15 @@ class DefaultTextStyle extends InheritedWidget { /// for the [BuildContext] where the widget is inserted, and any of the other /// arguments that are not null replace the corresponding properties on that /// same default text style. + /// + /// This constructor cannot be used to override the [maxLines] property of the + /// ancestor with the value null, since null here is used to mean "defer to + /// ancestor". To replace a non-null [maxLines] from an ancestor with the null + /// value (to remove the restriction on number of lines), manually obtain the + /// ambient [DefaultTextStyle] using [DefaultTextStyle.of], then create a new + /// [DefaultTextStyle] using the [new DefaultTextStyle] constructor directly. + /// See the source below for an example of how to do this (since that's + /// essentially what this constructor does). static Widget merge({ Key key, TextStyle style, @@ -91,6 +109,12 @@ class DefaultTextStyle extends InheritedWidget { /// An optional maximum number of lines for the text to span, wrapping if necessary. /// If the text exceeds the given number of lines, it will be truncated according /// to [overflow]. + /// + /// If this is 1, text will not wrap. Otherwise, text will be wrapped at the + /// edge of the box. + /// + /// If this is non-null, it will override even explicit null values of + /// [Text.maxLines]. final int maxLines; /// The closest instance of this class that encloses the given context. @@ -213,9 +237,17 @@ class Text extends StatelessWidget { /// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope. final double textScaleFactor; - /// An optional maximum number of lines the text is allowed to take up. + /// An optional maximum number of lines for the text to span, wrapping if necessary. /// If the text exceeds the given number of lines, it will be truncated according /// to [overflow]. + /// + /// If this is 1, text will not wrap. Otherwise, text will be wrapped at the + /// edge of the box. + /// + /// If this is null, but there is an ambient [DefaultTextStyle] that specifies + /// an explicit number for its [DefaultTextStyle.maxLines], then the + /// [DefaultTextStyle] value will take precedence. You can use a [RichText] + /// widget directly to entirely override the [DefaultTextStyle]. final int maxLines; @override @@ -232,7 +264,7 @@ class Text extends StatelessWidget { maxLines: maxLines ?? defaultTextStyle.maxLines, text: new TextSpan( style: effectiveTextStyle, - text: data + text: data, ) ); } diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index d727d3dc17..2fbdd0249c 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -46,7 +46,7 @@ void main() { 'First line of text is ' 'Second line goes until ' 'Third line of stuff '; - const String kFourLines = + const String kMoreThanFourLines = kThreeLines + 'Fourth line won\'t display and ends at'; @@ -462,7 +462,7 @@ void main() { ); } - await tester.pumpWidget(builder(3)); + await tester.pumpWidget(builder(null)); RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey)); @@ -470,28 +470,44 @@ void main() { final Size emptyInputSize = inputBox.size; await tester.enterText(find.byType(TextField), 'No wrapping here.'); - await tester.pumpWidget(builder(3)); + await tester.pumpWidget(builder(null)); expect(findInputBox(), equals(inputBox)); expect(inputBox.size, equals(emptyInputSize)); - await tester.enterText(find.byType(TextField), kThreeLines); await tester.pumpWidget(builder(3)); expect(findInputBox(), equals(inputBox)); expect(inputBox.size, greaterThan(emptyInputSize)); final Size threeLineInputSize = inputBox.size; + await tester.enterText(find.byType(TextField), kThreeLines); + await tester.pumpWidget(builder(null)); + expect(findInputBox(), equals(inputBox)); + expect(inputBox.size, greaterThan(emptyInputSize)); + + await tester.enterText(find.byType(TextField), kThreeLines); + await tester.pumpWidget(builder(null)); + expect(findInputBox(), equals(inputBox)); + expect(inputBox.size, threeLineInputSize); + // An extra line won't increase the size because we max at 3. - await tester.enterText(find.byType(TextField), kFourLines); + await tester.enterText(find.byType(TextField), kMoreThanFourLines); await tester.pumpWidget(builder(3)); expect(findInputBox(), equals(inputBox)); expect(inputBox.size, threeLineInputSize); - // But now it will. - await tester.enterText(find.byType(TextField), kFourLines); + // But now it will... but it will max at four + await tester.enterText(find.byType(TextField), kMoreThanFourLines); await tester.pumpWidget(builder(4)); expect(findInputBox(), equals(inputBox)); expect(inputBox.size, greaterThan(threeLineInputSize)); + + final Size fourLineInputSize = inputBox.size; + + // Now it won't max out until the end + await tester.pumpWidget(builder(null)); + expect(findInputBox(), equals(inputBox)); + expect(inputBox.size, greaterThan(fourLineInputSize)); }); testWidgets('Can drag handles to change selection in multiline', (WidgetTester tester) async { @@ -594,7 +610,7 @@ void main() { await tester.pumpWidget(builder()); await tester.pump(const Duration(seconds: 1)); - await tester.enterText(find.byType(TextField), kFourLines); + await tester.enterText(find.byType(TextField), kMoreThanFourLines); await tester.pumpWidget(builder()); await tester.pump(const Duration(seconds: 1)); @@ -603,8 +619,8 @@ void main() { final RenderBox inputBox = findInputBox(); // Check that the last line of text is not displayed. - final Offset firstPos = textOffsetToPosition(tester, kFourLines.indexOf('First')); - final Offset fourthPos = textOffsetToPosition(tester, kFourLines.indexOf('Fourth')); + final Offset firstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First')); + final Offset fourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth')); expect(firstPos.dx, fourthPos.dx); expect(firstPos.dy, lessThan(fourthPos.dy)); expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(firstPos)), isTrue); @@ -622,8 +638,8 @@ void main() { await tester.pump(); // Now the first line is scrolled up, and the fourth line is visible. - Offset newFirstPos = textOffsetToPosition(tester, kFourLines.indexOf('First')); - Offset newFourthPos = textOffsetToPosition(tester, kFourLines.indexOf('Fourth')); + Offset newFirstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First')); + Offset newFourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth')); expect(newFirstPos.dy, lessThan(firstPos.dy)); expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isFalse); @@ -633,7 +649,7 @@ void main() { // Long press the 'i' in 'Fourth line' to select the word. await tester.pump(const Duration(seconds: 1)); - final Offset untilPos = textOffsetToPosition(tester, kFourLines.indexOf('Fourth line')+8); + final Offset untilPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth line')+8); gesture = await tester.startGesture(untilPos, pointer: 7); await tester.pump(const Duration(seconds: 1)); await gesture.up(); @@ -645,7 +661,7 @@ void main() { // Drag the left handle to the first line, just after 'First'. final Offset handlePos = endpoints[0].point + const Offset(-1.0, 1.0); - final Offset newHandlePos = textOffsetToPosition(tester, kFourLines.indexOf('First') + 5); + final Offset newHandlePos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First') + 5); gesture = await tester.startGesture(handlePos, pointer: 7); await tester.pump(const Duration(seconds: 1)); await gesture.moveTo(newHandlePos + const Offset(0.0, -10.0)); @@ -655,8 +671,8 @@ void main() { // The text should have scrolled up with the handle to keep the active // cursor visible, back to its original position. - newFirstPos = textOffsetToPosition(tester, kFourLines.indexOf('First')); - newFourthPos = textOffsetToPosition(tester, kFourLines.indexOf('Fourth')); + newFirstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First')); + newFourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth')); expect(newFirstPos.dy, firstPos.dy); expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isTrue); expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isFalse); diff --git a/packages/flutter/test/rendering/paragraph_test.dart b/packages/flutter/test/rendering/paragraph_test.dart index 327157f9f7..16ebea1594 100644 --- a/packages/flutter/test/rendering/paragraph_test.dart +++ b/packages/flutter/test/rendering/paragraph_test.dart @@ -79,12 +79,13 @@ void main() { const TextSpan( text: 'This\n' // 4 characters * 10px font size = 40px width on the first line 'is a wrapping test. It should wrap at manual newlines, and if softWrap is true, also at spaces.', - style: const TextStyle(fontFamily: 'Ahem', fontSize: 10.0)), + style: const TextStyle(fontFamily: 'Ahem', fontSize: 10.0), + ), maxLines: 1, softWrap: true, ); - void relayoutWith({int maxLines, bool softWrap, TextOverflow overflow}) { + void relayoutWith({ int maxLines, bool softWrap, TextOverflow overflow }) { paragraph ..maxLines = maxLines ..softWrap = softWrap @@ -147,5 +148,34 @@ void main() { relayoutWith(maxLines: 100, softWrap: true, overflow: TextOverflow.fade); expect(paragraph.debugHasOverflowShader, isFalse); }); + + + test('maxLines', () { + final RenderParagraph paragraph = new RenderParagraph( + const TextSpan( + text: 'How do you write like you\'re running out of time? Write day and night like you\'re running out of time?', + // 0123456789 0123456789 012 345 0123456 012345 01234 012345678 012345678 0123 012 345 0123456 012345 01234 + // 0 1 2 3 4 5 6 7 8 9 10 11 12 + style: const TextStyle(fontFamily: 'Ahem', fontSize: 10.0), + ), + ); + layout(paragraph, constraints: const BoxConstraints(maxWidth: 100.0)); + void layoutAt(int maxLines) { + paragraph.maxLines = maxLines; + pumpFrame(); + } + + layoutAt(null); + expect(paragraph.size.height, 130.0); + + layoutAt(1); + expect(paragraph.size.height, 10.0); + + layoutAt(2); + expect(paragraph.size.height, 20.0); + + layoutAt(3); + expect(paragraph.size.height, 30.0); + }); }