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 { ...@@ -62,6 +62,12 @@ class TextField extends StatefulWidget {
/// To remove the decoration entirely (including the extra padding introduced /// To remove the decoration entirely (including the extra padding introduced
/// by the decoration to save space for the labels), set the [decoration] to /// by the decoration to save space for the labels), set the [decoration] to
/// null. /// 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({ const TextField({
Key key, Key key,
this.controller, this.controller,
...@@ -76,7 +82,11 @@ class TextField extends StatefulWidget { ...@@ -76,7 +82,11 @@ class TextField extends StatefulWidget {
this.onChanged, this.onChanged,
this.onSubmitted, this.onSubmitted,
this.inputFormatters, 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. /// Controls the text being edited.
/// ///
...@@ -98,6 +108,8 @@ class TextField extends StatefulWidget { ...@@ -98,6 +108,8 @@ class TextField extends StatefulWidget {
final InputDecoration decoration; final InputDecoration decoration;
/// The type of keyboard to use for editing the text. /// The type of keyboard to use for editing the text.
///
/// Defaults to [TextInputType.text]. Cannot be null.
final TextInputType keyboardType; final TextInputType keyboardType;
/// The style to use for the text being edited. /// The style to use for the text being edited.
...@@ -116,7 +128,7 @@ class TextField extends StatefulWidget { ...@@ -116,7 +128,7 @@ class TextField extends StatefulWidget {
/// If true, the keyboard will open as soon as this text field obtains focus. /// 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. /// 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 // See https://github.com/flutter/flutter/issues/7035 for the rationale for this
// keyboard behavior. // keyboard behavior.
final bool autofocus; final bool autofocus;
...@@ -126,13 +138,16 @@ class TextField extends StatefulWidget { ...@@ -126,13 +138,16 @@ class TextField extends StatefulWidget {
/// When this is set to true, all the characters in the text field are /// When this is set to true, all the characters in the text field are
/// replaced by U+2022 BULLET characters (•). /// replaced by U+2022 BULLET characters (•).
/// ///
/// Defaults to false. /// Defaults to false. Cannot be null.
final bool obscureText; final bool obscureText;
/// The maximum number of lines for the text to span, wrapping if necessary. /// 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 /// If this is 1 (the default), the text will not wrap, but will scroll
/// horizontally instead. /// 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; final int maxLines;
/// Called when the text being edited changes. /// Called when the text being edited changes.
......
...@@ -30,7 +30,8 @@ import 'text_field.dart'; ...@@ -30,7 +30,8 @@ import 'text_field.dart';
class TextFormField extends FormField<String> { class TextFormField extends FormField<String> {
/// Creates a [FormField] that contains a [TextField]. /// 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({ TextFormField({
Key key, Key key,
TextEditingController controller, TextEditingController controller,
...@@ -44,7 +45,11 @@ class TextFormField extends FormField<String> { ...@@ -44,7 +45,11 @@ class TextFormField extends FormField<String> {
FormFieldSetter<String> onSaved, FormFieldSetter<String> onSaved,
FormFieldValidator<String> validator, FormFieldValidator<String> validator,
List<TextInputFormatter> inputFormatters, List<TextInputFormatter> inputFormatters,
}) : super( }) : assert(keyboardType != null),
assert(autofocus != null),
assert(obscureText != null),
assert(maxLines == null || maxLines > 0),
super(
key: key, key: key,
initialValue: controller != null ? controller.value.text : '', initialValue: controller != null ? controller.value.text : '',
onSaved: onSaved, onSaved: onSaved,
......
...@@ -35,6 +35,8 @@ class TextPainter { ...@@ -35,6 +35,8 @@ class TextPainter {
/// ///
/// The text argument is optional but [text] must be non-null before calling /// The text argument is optional but [text] must be non-null before calling
/// [layout]. /// [layout].
///
/// The [maxLines] property, if non-null, must be greater than zero.
TextPainter({ TextPainter({
TextSpan text, TextSpan text,
TextAlign textAlign, TextAlign textAlign,
...@@ -43,6 +45,7 @@ class TextPainter { ...@@ -43,6 +45,7 @@ class TextPainter {
String ellipsis, String ellipsis,
}) : assert(text == null || text.debugAssertIsValid()), }) : assert(text == null || text.debugAssertIsValid()),
assert(textScaleFactor != null), assert(textScaleFactor != null),
assert(maxLines == null || maxLines > 0),
_text = text, _text = text,
_textAlign = textAlign, _textAlign = textAlign,
_textScaleFactor = textScaleFactor, _textScaleFactor = textScaleFactor,
...@@ -134,7 +137,9 @@ class TextPainter { ...@@ -134,7 +137,9 @@ class TextPainter {
/// After this is set, you must call [layout] before the next call to [paint]. /// After this is set, you must call [layout] before the next call to [paint].
int get maxLines => _maxLines; int get maxLines => _maxLines;
int _maxLines; int _maxLines;
/// The value may be null. If it is not null, then it must be greater than zero.
set maxLines(int value) { set maxLines(int value) {
assert(value == null || value > 0);
if (_maxLines == value) if (_maxLines == value)
return; return;
_maxLines = value; _maxLines = value;
......
...@@ -234,12 +234,18 @@ class TextStyle { ...@@ -234,12 +234,18 @@ class TextStyle {
} }
/// The style information for paragraphs, encoded for use by `dart:ui`. /// 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({ ui.ParagraphStyle getParagraphStyle({
TextAlign textAlign, TextAlign textAlign,
double textScaleFactor: 1.0, double textScaleFactor: 1.0,
String ellipsis, String ellipsis,
int maxLines, int maxLines,
}) { }) {
assert(textScaleFactor != null);
assert(maxLines == null || maxLines > 0);
return new ui.ParagraphStyle( return new ui.ParagraphStyle(
textAlign: textAlign, textAlign: textAlign,
fontWeight: fontWeight, fontWeight: fontWeight,
......
...@@ -84,6 +84,15 @@ class TextSelectionPoint { ...@@ -84,6 +84,15 @@ class TextSelectionPoint {
/// responsibility of higher layers and not handled by this object. /// responsibility of higher layers and not handled by this object.
class RenderEditable extends RenderBox { class RenderEditable extends RenderBox {
/// Creates a render object that implements the visual aspects of a text field. /// 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({ RenderEditable({
TextSpan text, TextSpan text,
TextAlign textAlign, TextAlign textAlign,
...@@ -96,7 +105,7 @@ class RenderEditable extends RenderBox { ...@@ -96,7 +105,7 @@ class RenderEditable extends RenderBox {
@required ViewportOffset offset, @required ViewportOffset offset,
this.onSelectionChanged, this.onSelectionChanged,
this.onCaretChanged, this.onCaretChanged,
}) : assert(maxLines != null), }) : assert(maxLines == null || maxLines > 0),
assert(textScaleFactor != null), assert(textScaleFactor != null),
assert(offset != null), assert(offset != null),
_textPainter = new TextPainter(text: text, textAlign: textAlign, textScaleFactor: textScaleFactor), _textPainter = new TextPainter(text: text, textAlign: textAlign, textScaleFactor: textScaleFactor),
...@@ -180,13 +189,21 @@ class RenderEditable extends RenderBox { ...@@ -180,13 +189,21 @@ class RenderEditable extends RenderBox {
} }
/// The maximum number of lines for the text to span, wrapping if necessary. /// 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 /// If this is 1 (the default), the text will not wrap, but will extend
/// indefinitely instead. /// 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 get maxLines => _maxLines;
int _maxLines; int _maxLines;
/// The value may be null. If it is not null, then it must be greater than zero.
set maxLines(int value) { set maxLines(int value) {
assert(value != null); assert(value == null || value > 0);
if (_maxLines == value) if (maxLines == value)
return; return;
_maxLines = value; _maxLines = value;
markNeedsTextLayout(); markNeedsTextLayout();
...@@ -261,7 +278,7 @@ class RenderEditable extends RenderBox { ...@@ -261,7 +278,7 @@ class RenderEditable extends RenderBox {
super.detach(); super.detach();
} }
bool get _isMultiline => maxLines > 1; bool get _isMultiline => maxLines != 1;
Axis get _viewportAxis => _isMultiline ? Axis.vertical : Axis.horizontal; Axis get _viewportAxis => _isMultiline ? Axis.vertical : Axis.horizontal;
...@@ -359,14 +376,30 @@ class RenderEditable extends RenderBox { ...@@ -359,14 +376,30 @@ class RenderEditable extends RenderBox {
// This does not required the layout to be updated. // This does not required the layout to be updated.
double get _preferredLineHeight => _textPainter.preferredLineHeight; 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 @override
double computeMinIntrinsicHeight(double width) { double computeMinIntrinsicHeight(double width) {
return _preferredLineHeight; return _preferredHeight(width);
} }
@override @override
double computeMaxIntrinsicHeight(double width) { double computeMaxIntrinsicHeight(double width) {
return _preferredLineHeight * maxLines; return _preferredHeight(width);
} }
@override @override
...@@ -434,7 +467,7 @@ class RenderEditable extends RenderBox { ...@@ -434,7 +467,7 @@ class RenderEditable extends RenderBox {
return; return;
final double caretMargin = _kCaretGap + _kCaretWidth; final double caretMargin = _kCaretGap + _kCaretWidth;
final double availableWidth = math.max(0.0, constraintWidth - caretMargin); 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); _textPainter.layout(minWidth: availableWidth, maxWidth: maxWidth);
_textLayoutLastWidth = constraintWidth; _textLayoutLastWidth = constraintWidth;
} }
...@@ -444,9 +477,7 @@ class RenderEditable extends RenderBox { ...@@ -444,9 +477,7 @@ class RenderEditable extends RenderBox {
_layoutText(constraints.maxWidth); _layoutText(constraints.maxWidth);
_caretPrototype = new Rect.fromLTWH(0.0, _kCaretHeightOffset, _kCaretWidth, _preferredLineHeight - 2.0 * _kCaretHeightOffset); _caretPrototype = new Rect.fromLTWH(0.0, _kCaretHeightOffset, _kCaretWidth, _preferredLineHeight - 2.0 * _kCaretHeightOffset);
_selectionRects = null; _selectionRects = null;
size = new Size(constraints.maxWidth, constraints.constrainHeight( size = new Size(constraints.maxWidth, constraints.constrainHeight(_preferredHeight(constraints.maxWidth)));
_textPainter.height.clamp(_preferredLineHeight, _preferredLineHeight * _maxLines)
));
final Size contentSize = new Size(_textPainter.width + _kCaretGap + _kCaretWidth, _textPainter.height); final Size contentSize = new Size(_textPainter.width + _kCaretGap + _kCaretWidth, _textPainter.height);
final double _maxScrollExtent = _getMaxScrollExtent(contentSize); final double _maxScrollExtent = _getMaxScrollExtent(contentSize);
_hasVisualOverflow = _maxScrollExtent > 0.0; _hasVisualOverflow = _maxScrollExtent > 0.0;
...@@ -506,13 +537,13 @@ class RenderEditable extends RenderBox { ...@@ -506,13 +537,13 @@ class RenderEditable extends RenderBox {
@override @override
void debugFillDescription(List<String> description) { void debugFillDescription(List<String> description) {
super.debugFillDescription(description); super.debugFillDescription(description);
description.add('cursorColor: $_cursorColor'); description.add('cursorColor: $cursorColor');
description.add('showCursor: $_showCursor'); description.add('showCursor: $showCursor');
description.add('maxLines: $_maxLines'); description.add('maxLines: $maxLines');
description.add('selectionColor: $_selectionColor'); description.add('selectionColor: $selectionColor');
description.add('textScaleFactor: $textScaleFactor'); description.add('textScaleFactor: $textScaleFactor');
description.add('selection: $_selection'); description.add('selection: $selection');
description.add('offset: $_offset'); description.add('offset: $offset');
} }
@override @override
......
...@@ -31,7 +31,11 @@ const String _kEllipsis = '\u2026'; ...@@ -31,7 +31,11 @@ const String _kEllipsis = '\u2026';
class RenderParagraph extends RenderBox { class RenderParagraph extends RenderBox {
/// Creates a paragraph render object. /// 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, { RenderParagraph(TextSpan text, {
TextAlign textAlign, TextAlign textAlign,
bool softWrap: true, bool softWrap: true,
...@@ -43,6 +47,7 @@ class RenderParagraph extends RenderBox { ...@@ -43,6 +47,7 @@ class RenderParagraph extends RenderBox {
assert(softWrap != null), assert(softWrap != null),
assert(overflow != null), assert(overflow != null),
assert(textScaleFactor != null), assert(textScaleFactor != null),
assert(maxLines == null || maxLines > 0),
_softWrap = softWrap, _softWrap = softWrap,
_overflow = overflow, _overflow = overflow,
_textPainter = new TextPainter( _textPainter = new TextPainter(
...@@ -77,7 +82,11 @@ class RenderParagraph extends RenderBox { ...@@ -77,7 +82,11 @@ class RenderParagraph extends RenderBox {
/// Whether the text should break at soft line breaks. /// 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 get softWrap => _softWrap;
bool _softWrap; bool _softWrap;
set softWrap(bool value) { set softWrap(bool value) {
...@@ -116,9 +125,11 @@ class RenderParagraph extends RenderBox { ...@@ -116,9 +125,11 @@ class RenderParagraph extends RenderBox {
/// An optional maximum number of lines for the text to span, wrapping if necessary. /// 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 /// 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; 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) { set maxLines(int value) {
assert(value == null || value > 0);
if (_textPainter.maxLines == value) if (_textPainter.maxLines == value)
return; return;
_textPainter.maxLines = value; _textPainter.maxLines = value;
...@@ -127,8 +138,7 @@ class RenderParagraph extends RenderBox { ...@@ -127,8 +138,7 @@ class RenderParagraph extends RenderBox {
} }
void _layoutText({ double minWidth: 0.0, double maxWidth: double.INFINITY }) { void _layoutText({ double minWidth: 0.0, double maxWidth: double.INFINITY }) {
final bool wrap = _softWrap || (_overflow == TextOverflow.ellipsis && maxLines == null); _textPainter.layout(minWidth: minWidth, maxWidth: _softWrap ? maxWidth : double.INFINITY);
_textPainter.layout(minWidth: minWidth, maxWidth: wrap ? maxWidth : double.INFINITY);
} }
void _layoutTextWithConstraints(BoxConstraints constraints) { void _layoutTextWithConstraints(BoxConstraints constraints) {
......
...@@ -3028,7 +3028,11 @@ class Flow extends MultiChildRenderObjectWidget { ...@@ -3028,7 +3028,11 @@ class Flow extends MultiChildRenderObjectWidget {
class RichText extends LeafRenderObjectWidget { class RichText extends LeafRenderObjectWidget {
/// Creates a paragraph of rich text. /// 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({ const RichText({
Key key, Key key,
@required this.text, @required this.text,
...@@ -3041,6 +3045,7 @@ class RichText extends LeafRenderObjectWidget { ...@@ -3041,6 +3045,7 @@ class RichText extends LeafRenderObjectWidget {
assert(softWrap != null), assert(softWrap != null),
assert(overflow != null), assert(overflow != null),
assert(textScaleFactor != null), assert(textScaleFactor != null),
assert(maxLines == null || maxLines > 0),
super(key: key); super(key: key);
/// The text to display in this widget. /// The text to display in this widget.
...@@ -3066,6 +3071,9 @@ class RichText extends LeafRenderObjectWidget { ...@@ -3066,6 +3071,9 @@ class RichText extends LeafRenderObjectWidget {
/// An optional maximum number of lines for the text to span, wrapping if necessary. /// 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 /// If the text exceeds the given number of lines, it will be truncated according
/// to [overflow]. /// to [overflow].
///
/// If this is 1, text will not wrap. Otherwise, text will be wrapped at the
/// edge of the box.
final int maxLines; final int maxLines;
@override @override
......
...@@ -128,6 +128,10 @@ class TextEditingController extends ValueNotifier<TextEditingValue> { ...@@ -128,6 +128,10 @@ class TextEditingController extends ValueNotifier<TextEditingValue> {
class EditableText extends StatefulWidget { class EditableText extends StatefulWidget {
/// Creates a basic text input control. /// 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 /// The [controller], [focusNode], [style], and [cursorColor] arguments must
/// not be null. /// not be null.
EditableText({ EditableText({
...@@ -152,7 +156,7 @@ class EditableText extends StatefulWidget { ...@@ -152,7 +156,7 @@ class EditableText extends StatefulWidget {
assert(obscureText != null), assert(obscureText != null),
assert(style != null), assert(style != null),
assert(cursorColor != null), assert(cursorColor != null),
assert(maxLines != null), assert(maxLines == null || maxLines > 0),
assert(autofocus != null), assert(autofocus != null),
inputFormatters = maxLines == 1 inputFormatters = maxLines == 1
? ( ? (
...@@ -192,8 +196,12 @@ class EditableText extends StatefulWidget { ...@@ -192,8 +196,12 @@ class EditableText extends StatefulWidget {
final Color cursorColor; final Color cursorColor;
/// The maximum number of lines for the text to span, wrapping if necessary. /// 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 /// If this is 1 (the default), the text will not wrap, but will scroll
/// horizontally instead. /// 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; final int maxLines;
/// Whether this input field should focus itself if nothing else is already focused. /// Whether this input field should focus itself if nothing else is already focused.
...@@ -340,7 +348,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -340,7 +348,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
} }
bool get _hasFocus => widget.focusNode.hasFocus; 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. // Calculate the new scroll offset so the cursor remains visible.
double _getScrollOffsetForCaret(Rect caretRect) { double _getScrollOffsetForCaret(Rect caretRect) {
...@@ -417,7 +425,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -417,7 +425,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
void _handleSelectionChanged(TextSelection selection, RenderEditable renderObject, bool longPress) { void _handleSelectionChanged(TextSelection selection, RenderEditable renderObject, bool longPress) {
widget.controller.selection = selection; 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. // EditableWidget, not just changes triggered by user gestures.
requestKeyboard(); requestKeyboard();
......
...@@ -14,6 +14,14 @@ class DefaultTextStyle extends InheritedWidget { ...@@ -14,6 +14,14 @@ class DefaultTextStyle extends InheritedWidget {
/// ///
/// Consider using [DefaultTextStyle.merge] to inherit styling information /// Consider using [DefaultTextStyle.merge] to inherit styling information
/// from the current default text style for a given [BuildContext]. /// 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({ const DefaultTextStyle({
Key key, Key key,
@required this.style, @required this.style,
...@@ -25,6 +33,7 @@ class DefaultTextStyle extends InheritedWidget { ...@@ -25,6 +33,7 @@ class DefaultTextStyle extends InheritedWidget {
}) : assert(style != null), }) : assert(style != null),
assert(softWrap != null), assert(softWrap != null),
assert(overflow != null), assert(overflow != null),
assert(maxLines == null || maxLines > 0),
assert(child != null), assert(child != null),
super(key: key, child: child); super(key: key, child: child);
...@@ -48,6 +57,15 @@ class DefaultTextStyle extends InheritedWidget { ...@@ -48,6 +57,15 @@ class DefaultTextStyle extends InheritedWidget {
/// for the [BuildContext] where the widget is inserted, and any of the other /// for the [BuildContext] where the widget is inserted, and any of the other
/// arguments that are not null replace the corresponding properties on that /// arguments that are not null replace the corresponding properties on that
/// same default text style. /// 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({ static Widget merge({
Key key, Key key,
TextStyle style, TextStyle style,
...@@ -91,6 +109,12 @@ class DefaultTextStyle extends InheritedWidget { ...@@ -91,6 +109,12 @@ class DefaultTextStyle extends InheritedWidget {
/// An optional maximum number of lines for the text to span, wrapping if necessary. /// 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 /// If the text exceeds the given number of lines, it will be truncated according
/// to [overflow]. /// 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; final int maxLines;
/// The closest instance of this class that encloses the given context. /// The closest instance of this class that encloses the given context.
...@@ -213,9 +237,17 @@ class Text extends StatelessWidget { ...@@ -213,9 +237,17 @@ class Text extends StatelessWidget {
/// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope. /// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope.
final double textScaleFactor; 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 /// If the text exceeds the given number of lines, it will be truncated according
/// to [overflow]. /// 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; final int maxLines;
@override @override
...@@ -232,7 +264,7 @@ class Text extends StatelessWidget { ...@@ -232,7 +264,7 @@ class Text extends StatelessWidget {
maxLines: maxLines ?? defaultTextStyle.maxLines, maxLines: maxLines ?? defaultTextStyle.maxLines,
text: new TextSpan( text: new TextSpan(
style: effectiveTextStyle, style: effectiveTextStyle,
text: data text: data,
) )
); );
} }
......
...@@ -46,7 +46,7 @@ void main() { ...@@ -46,7 +46,7 @@ void main() {
'First line of text is ' 'First line of text is '
'Second line goes until ' 'Second line goes until '
'Third line of stuff '; 'Third line of stuff ';
const String kFourLines = const String kMoreThanFourLines =
kThreeLines + kThreeLines +
'Fourth line won\'t display and ends at'; 'Fourth line won\'t display and ends at';
...@@ -462,7 +462,7 @@ void main() { ...@@ -462,7 +462,7 @@ void main() {
); );
} }
await tester.pumpWidget(builder(3)); await tester.pumpWidget(builder(null));
RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey)); RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
...@@ -470,28 +470,44 @@ void main() { ...@@ -470,28 +470,44 @@ void main() {
final Size emptyInputSize = inputBox.size; final Size emptyInputSize = inputBox.size;
await tester.enterText(find.byType(TextField), 'No wrapping here.'); await tester.enterText(find.byType(TextField), 'No wrapping here.');
await tester.pumpWidget(builder(3)); await tester.pumpWidget(builder(null));
expect(findInputBox(), equals(inputBox)); expect(findInputBox(), equals(inputBox));
expect(inputBox.size, equals(emptyInputSize)); expect(inputBox.size, equals(emptyInputSize));
await tester.enterText(find.byType(TextField), kThreeLines);
await tester.pumpWidget(builder(3)); await tester.pumpWidget(builder(3));
expect(findInputBox(), equals(inputBox)); expect(findInputBox(), equals(inputBox));
expect(inputBox.size, greaterThan(emptyInputSize)); expect(inputBox.size, greaterThan(emptyInputSize));
final Size threeLineInputSize = inputBox.size; 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. // 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)); await tester.pumpWidget(builder(3));
expect(findInputBox(), equals(inputBox)); expect(findInputBox(), equals(inputBox));
expect(inputBox.size, threeLineInputSize); expect(inputBox.size, threeLineInputSize);
// But now it will. // But now it will... but it will max at four
await tester.enterText(find.byType(TextField), kFourLines); await tester.enterText(find.byType(TextField), kMoreThanFourLines);
await tester.pumpWidget(builder(4)); await tester.pumpWidget(builder(4));
expect(findInputBox(), equals(inputBox)); expect(findInputBox(), equals(inputBox));
expect(inputBox.size, greaterThan(threeLineInputSize)); 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 { testWidgets('Can drag handles to change selection in multiline', (WidgetTester tester) async {
...@@ -594,7 +610,7 @@ void main() { ...@@ -594,7 +610,7 @@ void main() {
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
await tester.pump(const Duration(seconds: 1)); 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.pumpWidget(builder());
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
...@@ -603,8 +619,8 @@ void main() { ...@@ -603,8 +619,8 @@ void main() {
final RenderBox inputBox = findInputBox(); final RenderBox inputBox = findInputBox();
// Check that the last line of text is not displayed. // Check that the last line of text is not displayed.
final Offset firstPos = textOffsetToPosition(tester, kFourLines.indexOf('First')); final Offset firstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First'));
final Offset fourthPos = textOffsetToPosition(tester, kFourLines.indexOf('Fourth')); final Offset fourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth'));
expect(firstPos.dx, fourthPos.dx); expect(firstPos.dx, fourthPos.dx);
expect(firstPos.dy, lessThan(fourthPos.dy)); expect(firstPos.dy, lessThan(fourthPos.dy));
expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(firstPos)), isTrue); expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(firstPos)), isTrue);
...@@ -622,8 +638,8 @@ void main() { ...@@ -622,8 +638,8 @@ void main() {
await tester.pump(); await tester.pump();
// Now the first line is scrolled up, and the fourth line is visible. // Now the first line is scrolled up, and the fourth line is visible.
Offset newFirstPos = textOffsetToPosition(tester, kFourLines.indexOf('First')); Offset newFirstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First'));
Offset newFourthPos = textOffsetToPosition(tester, kFourLines.indexOf('Fourth')); Offset newFourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth'));
expect(newFirstPos.dy, lessThan(firstPos.dy)); expect(newFirstPos.dy, lessThan(firstPos.dy));
expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isFalse); expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isFalse);
...@@ -633,7 +649,7 @@ void main() { ...@@ -633,7 +649,7 @@ void main() {
// Long press the 'i' in 'Fourth line' to select the word. // Long press the 'i' in 'Fourth line' to select the word.
await tester.pump(const Duration(seconds: 1)); 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); gesture = await tester.startGesture(untilPos, pointer: 7);
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
await gesture.up(); await gesture.up();
...@@ -645,7 +661,7 @@ void main() { ...@@ -645,7 +661,7 @@ void main() {
// Drag the left handle to the first line, just after 'First'. // 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 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); gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
await gesture.moveTo(newHandlePos + const Offset(0.0, -10.0)); await gesture.moveTo(newHandlePos + const Offset(0.0, -10.0));
...@@ -655,8 +671,8 @@ void main() { ...@@ -655,8 +671,8 @@ void main() {
// The text should have scrolled up with the handle to keep the active // The text should have scrolled up with the handle to keep the active
// cursor visible, back to its original position. // cursor visible, back to its original position.
newFirstPos = textOffsetToPosition(tester, kFourLines.indexOf('First')); newFirstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First'));
newFourthPos = textOffsetToPosition(tester, kFourLines.indexOf('Fourth')); newFourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth'));
expect(newFirstPos.dy, firstPos.dy); expect(newFirstPos.dy, firstPos.dy);
expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isTrue); expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isTrue);
expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isFalse); expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isFalse);
......
...@@ -79,12 +79,13 @@ void main() { ...@@ -79,12 +79,13 @@ void main() {
const TextSpan( const TextSpan(
text: 'This\n' // 4 characters * 10px font size = 40px width on the first line 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.', '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, maxLines: 1,
softWrap: true, softWrap: true,
); );
void relayoutWith({int maxLines, bool softWrap, TextOverflow overflow}) { void relayoutWith({ int maxLines, bool softWrap, TextOverflow overflow }) {
paragraph paragraph
..maxLines = maxLines ..maxLines = maxLines
..softWrap = softWrap ..softWrap = softWrap
...@@ -147,5 +148,34 @@ void main() { ...@@ -147,5 +148,34 @@ void main() {
relayoutWith(maxLines: 100, softWrap: true, overflow: TextOverflow.fade); relayoutWith(maxLines: 100, softWrap: true, overflow: TextOverflow.fade);
expect(paragraph.debugHasOverflowShader, isFalse); 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