Commit d74a5883 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Allow multi-line text fields with no line limit (#10576)

parent db75aa76
......@@ -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.
......
......@@ -30,7 +30,8 @@ import 'text_field.dart';
class TextFormField extends FormField<String> {
/// 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<String> {
FormFieldSetter<String> onSaved,
FormFieldValidator<String> validator,
List<TextInputFormatter> 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,
......
......@@ -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;
......
......@@ -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,
......
......@@ -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<String> 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
......
......@@ -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) {
......
......@@ -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
......
......@@ -128,6 +128,10 @@ class TextEditingController extends ValueNotifier<TextEditingValue> {
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
? (
<TextInputFormatter>[BlacklistingTextInputFormatter.singleLineFormatter]
..addAll(inputFormatters ?? const Iterable<TextInputFormatter>.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<String> 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<TextInputFormatter> inputFormatters;
......@@ -340,7 +348,7 @@ class EditableTextState extends State<EditableText> 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<EditableText> 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();
......
......@@ -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,
)
);
}
......
......@@ -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);
......
......@@ -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);
});
}
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