Commit 2fd78dcc authored by Matt Perry's avatar Matt Perry Committed by GitHub

Add multiline support to Input and friends. (#6155)

Fixes https://github.com/flutter/flutter/issues/6154
parent 73ff4198
...@@ -94,6 +94,12 @@ class TextFieldDemoState extends State<TextFieldDemo> { ...@@ -94,6 +94,12 @@ class TextFieldDemoState extends State<TextFieldDemo> {
validator: _validatePhoneNumber validator: _validatePhoneNumber
) )
), ),
new Input(
hintText: 'Tell us about yourself (optional)',
labelText: 'Life story',
multiline: true,
formField: new FormField<String>()
),
new Row( new Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
......
...@@ -43,6 +43,7 @@ class Input extends StatefulWidget { ...@@ -43,6 +43,7 @@ class Input extends StatefulWidget {
this.hideText: false, this.hideText: false,
this.isDense: false, this.isDense: false,
this.autofocus: false, this.autofocus: false,
this.multiline: false,
this.formField, this.formField,
this.onChanged, this.onChanged,
this.onSubmitted this.onSubmitted
...@@ -84,6 +85,10 @@ class Input extends StatefulWidget { ...@@ -84,6 +85,10 @@ class Input extends StatefulWidget {
/// Whether this input field should focus itself is nothing else is already focused. /// Whether this input field should focus itself is nothing else is already focused.
final bool autofocus; final bool autofocus;
/// True if the text should wrap and span multiple lines, false if it should
/// stay on a single line and scroll when overflowed.
final bool multiline;
/// Form-specific data, required if this Input is part of a Form. /// Form-specific data, required if this Input is part of a Form.
final FormField<String> formField; final FormField<String> formField;
...@@ -205,10 +210,10 @@ class _InputState extends State<Input> { ...@@ -205,10 +210,10 @@ class _InputState extends State<Input> {
focusKey: focusKey, focusKey: focusKey,
style: textStyle, style: textStyle,
hideText: config.hideText, hideText: config.hideText,
multiline: config.multiline,
cursorColor: themeData.textSelectionColor, cursorColor: themeData.textSelectionColor,
selectionColor: themeData.textSelectionColor, selectionColor: themeData.textSelectionColor,
selectionHandleBuilder: buildTextSelectionHandle, selectionControls: materialTextSelectionControls,
selectionToolbarBuilder: buildTextSelectionToolbar,
platform: Theme.of(context).platform, platform: Theme.of(context).platform,
keyboardType: config.keyboardType, keyboardType: config.keyboardType,
onChanged: onChanged, onChanged: onChanged,
......
...@@ -143,8 +143,13 @@ class _TextSelectionHandlePainter extends CustomPainter { ...@@ -143,8 +143,13 @@ class _TextSelectionHandlePainter extends CustomPainter {
} }
} }
/// Builder for material-style copy/paste text selection toolbar. class _MaterialTextSelectionControls extends TextSelectionControls {
Widget buildTextSelectionToolbar( @override
Size handleSize = const Size(_kHandleSize, _kHandleSize);
/// Builder for material-style copy/paste text selection toolbar.
@override
Widget buildToolbar(
BuildContext context, Point position, TextSelectionDelegate delegate) { BuildContext context, Point position, TextSelectionDelegate delegate) {
final Size screenSize = MediaQuery.of(context).size; final Size screenSize = MediaQuery.of(context).size;
return new ConstrainedBox( return new ConstrainedBox(
...@@ -154,11 +159,11 @@ Widget buildTextSelectionToolbar( ...@@ -154,11 +159,11 @@ Widget buildTextSelectionToolbar(
child: new _TextSelectionToolbar(delegate) child: new _TextSelectionToolbar(delegate)
) )
); );
} }
/// Builder for material-style text selection handles. /// Builder for material-style text selection handles.
Widget buildTextSelectionHandle( @override
BuildContext context, TextSelectionHandleType type) { Widget buildHandle(BuildContext context, TextSelectionHandleType type) {
Widget handle = new SizedBox( Widget handle = new SizedBox(
width: _kHandleSize, width: _kHandleSize,
height: _kHandleSize, height: _kHandleSize,
...@@ -188,4 +193,7 @@ Widget buildTextSelectionHandle( ...@@ -188,4 +193,7 @@ Widget buildTextSelectionHandle(
} }
assert(type != null); assert(type != null);
return null; return null;
}
} }
final _MaterialTextSelectionControls materialTextSelectionControls = new _MaterialTextSelectionControls();
...@@ -219,7 +219,7 @@ class TextPainter { ...@@ -219,7 +219,7 @@ class TextPainter {
ui.TextBox box = boxes[0]; ui.TextBox box = boxes[0];
double caretEnd = box.end; double caretEnd = box.end;
double dx = box.direction == TextDirection.rtl ? caretEnd : caretEnd - caretPrototype.width; double dx = box.direction == TextDirection.rtl ? caretEnd : caretEnd - caretPrototype.width;
return new Offset(dx, 0.0); return new Offset(dx, box.top);
} }
Offset _getOffsetFromDownstream(int offset, Rect caretPrototype) { Offset _getOffsetFromDownstream(int offset, Rect caretPrototype) {
...@@ -229,7 +229,7 @@ class TextPainter { ...@@ -229,7 +229,7 @@ class TextPainter {
ui.TextBox box = boxes[0]; ui.TextBox box = boxes[0];
double caretStart = box.start; double caretStart = box.start;
double dx = box.direction == TextDirection.rtl ? caretStart - caretPrototype.width : caretStart; double dx = box.direction == TextDirection.rtl ? caretStart - caretPrototype.width : caretStart;
return new Offset(dx, 0.0); return new Offset(dx, box.top);
} }
/// Returns the offset at which to paint the caret. /// Returns the offset at which to paint the caret.
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui' as ui show Paragraph, ParagraphBuilder, ParagraphConstraints, ParagraphStyle, TextBox; import 'dart:ui' as ui show Paragraph, ParagraphBuilder, ParagraphConstraints, ParagraphStyle, TextBox;
import 'dart:math' as math;
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
...@@ -43,15 +44,17 @@ class RenderEditableLine extends RenderBox { ...@@ -43,15 +44,17 @@ class RenderEditableLine extends RenderBox {
TextSpan text, TextSpan text,
Color cursorColor, Color cursorColor,
bool showCursor: false, bool showCursor: false,
bool multiline: false,
Color selectionColor, Color selectionColor,
double textScaleFactor: 1.0, double textScaleFactor: 1.0,
TextSelection selection, TextSelection selection,
this.onSelectionChanged, this.onSelectionChanged,
Offset paintOffset: Offset.zero, Offset paintOffset: Offset.zero,
this.onPaintOffsetUpdateNeeded this.onPaintOffsetUpdateNeeded,
}) : _textPainter = new TextPainter(text: text, textScaleFactor: textScaleFactor), }) : _textPainter = new TextPainter(text: text, textScaleFactor: textScaleFactor),
_cursorColor = cursorColor, _cursorColor = cursorColor,
_showCursor = showCursor, _showCursor = showCursor,
_multiline = multiline,
_selection = selection, _selection = selection,
_paintOffset = paintOffset { _paintOffset = paintOffset {
assert(!showCursor || cursorColor != null); assert(!showCursor || cursorColor != null);
...@@ -163,14 +166,14 @@ class RenderEditableLine extends RenderBox { ...@@ -163,14 +166,14 @@ class RenderEditableLine extends RenderBox {
// TODO(mpcomplete): We should be more disciplined about when we dirty the // TODO(mpcomplete): We should be more disciplined about when we dirty the
// layout state of the text painter so that we can know that the layout is // layout state of the text painter so that we can know that the layout is
// clean at this point. // clean at this point.
_textPainter.layout(); _textPainter.layout(maxWidth: _maxContentWidth);
Offset offset = _paintOffset; Offset offset = _paintOffset;
if (selection.isCollapsed) { if (selection.isCollapsed) {
// TODO(mpcomplete): This doesn't work well at an RTL/LTR boundary. // TODO(mpcomplete): This doesn't work well at an RTL/LTR boundary.
Offset caretOffset = _textPainter.getOffsetForCaret(selection.extent, _caretPrototype); Offset caretOffset = _textPainter.getOffsetForCaret(selection.extent, _caretPrototype);
Point start = new Point(caretOffset.dx, size.height) + offset; Point start = new Point(0.0, constraints.constrainHeight(_preferredHeight)) + caretOffset + offset;
return <TextSelectionPoint>[new TextSelectionPoint(localToGlobal(start), null)]; return <TextSelectionPoint>[new TextSelectionPoint(localToGlobal(start), null)];
} else { } else {
List<ui.TextBox> boxes = _textPainter.getBoxesForSelection(selection); List<ui.TextBox> boxes = _textPainter.getBoxesForSelection(selection);
...@@ -200,11 +203,18 @@ class RenderEditableLine extends RenderBox { ...@@ -200,11 +203,18 @@ class RenderEditableLine extends RenderBox {
// TODO(abarth): ParagraphBuilder#build's argument should be optional. // TODO(abarth): ParagraphBuilder#build's argument should be optional.
// TODO(abarth): These min/max values should be the default for ui.Paragraph. // TODO(abarth): These min/max values should be the default for ui.Paragraph.
_layoutTemplate = builder.build(new ui.ParagraphStyle()) _layoutTemplate = builder.build(new ui.ParagraphStyle())
..layout(new ui.ParagraphConstraints(width: double.INFINITY)); ..layout(new ui.ParagraphConstraints(width: _maxContentWidth));
} }
return _layoutTemplate.height; return _layoutTemplate.height;
} }
bool _multiline;
double get _maxContentWidth {
return _multiline ?
constraints.maxWidth - (_kCaretGap + _kCaretWidth) :
double.INFINITY;
}
@override @override
double computeMinIntrinsicHeight(double width) { double computeMinIntrinsicHeight(double width) {
return _preferredHeight; return _preferredHeight;
...@@ -274,10 +284,11 @@ class RenderEditableLine extends RenderBox { ...@@ -274,10 +284,11 @@ class RenderEditableLine extends RenderBox {
@override @override
void performLayout() { void performLayout() {
Size oldSize = hasSize ? size : null; Size oldSize = hasSize ? size : null;
size = new Size(constraints.maxWidth, constraints.constrainHeight(_preferredHeight)); double lineHeight = constraints.constrainHeight(_preferredHeight);
_caretPrototype = new Rect.fromLTWH(0.0, _kCaretHeightOffset, _kCaretWidth, size.height - 2.0 * _kCaretHeightOffset); _caretPrototype = new Rect.fromLTWH(0.0, _kCaretHeightOffset, _kCaretWidth, lineHeight - 2.0 * _kCaretHeightOffset);
_selectionRects = null; _selectionRects = null;
_textPainter.layout(); _textPainter.layout(maxWidth: _maxContentWidth);
size = new Size(constraints.maxWidth, constraints.constrainHeight(math.max(lineHeight, _textPainter.height)));
Size contentSize = new Size(_textPainter.width + _kCaretGap + _kCaretWidth, _textPainter.height); Size contentSize = new Size(_textPainter.width + _kCaretGap + _kCaretWidth, _textPainter.height);
if (onPaintOffsetUpdateNeeded != null && (size != oldSize || contentSize != _contentSize)) if (onPaintOffsetUpdateNeeded != null && (size != oldSize || contentSize != _contentSize))
onPaintOffsetUpdateNeeded(new ViewportDimensions(containerSize: size, contentSize: contentSize)); onPaintOffsetUpdateNeeded(new ViewportDimensions(containerSize: size, contentSize: contentSize));
......
...@@ -156,6 +156,8 @@ class InputValue { ...@@ -156,6 +156,8 @@ class InputValue {
/// ///
/// This control is not intended to be used directly. Instead, consider using /// This control is not intended to be used directly. Instead, consider using
/// [Input], which provides focus management and material design. /// [Input], which provides focus management and material design.
//
// TODO(mpcomplete): rename RawInput since it can span multiple lines.
class RawInputLine extends Scrollable { class RawInputLine extends Scrollable {
/// Creates a basic single-line input control. /// Creates a basic single-line input control.
/// ///
...@@ -168,9 +170,9 @@ class RawInputLine extends Scrollable { ...@@ -168,9 +170,9 @@ class RawInputLine extends Scrollable {
this.style, this.style,
this.cursorColor, this.cursorColor,
this.textScaleFactor, this.textScaleFactor,
this.multiline,
this.selectionColor, this.selectionColor,
this.selectionHandleBuilder, this.selectionControls,
this.selectionToolbarBuilder,
@required this.platform, @required this.platform,
this.keyboardType, this.keyboardType,
this.onChanged, this.onChanged,
...@@ -206,16 +208,15 @@ class RawInputLine extends Scrollable { ...@@ -206,16 +208,15 @@ class RawInputLine extends Scrollable {
/// The color to use when painting the cursor. /// The color to use when painting the cursor.
final Color cursorColor; final Color cursorColor;
/// True if the text should wrap and span multiple lines, false if it should
/// stay on a single line and scroll when overflowed.
final bool multiline;
/// The color to use when painting the selection. /// The color to use when painting the selection.
final Color selectionColor; final Color selectionColor;
/// Optional builder function for a widget that controls the boundary of a /// Optional delegate for building the text selection handles and toolbar.
/// text selection. final TextSelectionControls selectionControls;
final TextSelectionHandleBuilder selectionHandleBuilder;
/// Optional builder function for a set of controls for working with a
/// text selection (e.g. copy and paste).
final TextSelectionToolbarBuilder selectionToolbarBuilder;
/// The platform whose behavior should be approximated, in particular /// The platform whose behavior should be approximated, in particular
/// for scroll physics. (See [ScrollBehavior.platform].) /// for scroll physics. (See [ScrollBehavior.platform].)
...@@ -356,15 +357,14 @@ class RawInputLineState extends ScrollableState<RawInputLine> { ...@@ -356,15 +357,14 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
_selectionOverlay = null; _selectionOverlay = null;
} }
if (config.selectionHandleBuilder != null) { if (config.selectionControls != null) {
_selectionOverlay = new TextSelectionOverlay( _selectionOverlay = new TextSelectionOverlay(
input: newInput, input: newInput,
context: context, context: context,
debugRequiredFor: config, debugRequiredFor: config,
renderObject: renderObject, renderObject: renderObject,
onSelectionOverlayChanged: _handleSelectionOverlayChanged, onSelectionOverlayChanged: _handleSelectionOverlayChanged,
handleBuilder: config.selectionHandleBuilder, selectionControls: config.selectionControls,
toolbarBuilder: config.selectionToolbarBuilder
); );
if (newInput.text.isNotEmpty || longPress) if (newInput.text.isNotEmpty || longPress)
_selectionOverlay.showHandles(); _selectionOverlay.showHandles();
...@@ -443,6 +443,7 @@ class RawInputLineState extends ScrollableState<RawInputLine> { ...@@ -443,6 +443,7 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
style: config.style, style: config.style,
cursorColor: config.cursorColor, cursorColor: config.cursorColor,
showCursor: _showCursor, showCursor: _showCursor,
multiline: config.multiline,
selectionColor: config.selectionColor, selectionColor: config.selectionColor,
textScaleFactor: config.textScaleFactor ?? MediaQuery.of(context).textScaleFactor, textScaleFactor: config.textScaleFactor ?? MediaQuery.of(context).textScaleFactor,
hideText: config.hideText, hideText: config.hideText,
...@@ -460,6 +461,7 @@ class _EditableLineWidget extends LeafRenderObjectWidget { ...@@ -460,6 +461,7 @@ class _EditableLineWidget extends LeafRenderObjectWidget {
this.style, this.style,
this.cursorColor, this.cursorColor,
this.showCursor, this.showCursor,
this.multiline,
this.selectionColor, this.selectionColor,
this.textScaleFactor, this.textScaleFactor,
this.hideText, this.hideText,
...@@ -472,6 +474,7 @@ class _EditableLineWidget extends LeafRenderObjectWidget { ...@@ -472,6 +474,7 @@ class _EditableLineWidget extends LeafRenderObjectWidget {
final TextStyle style; final TextStyle style;
final Color cursorColor; final Color cursorColor;
final bool showCursor; final bool showCursor;
final bool multiline;
final Color selectionColor; final Color selectionColor;
final double textScaleFactor; final double textScaleFactor;
final bool hideText; final bool hideText;
...@@ -485,6 +488,7 @@ class _EditableLineWidget extends LeafRenderObjectWidget { ...@@ -485,6 +488,7 @@ class _EditableLineWidget extends LeafRenderObjectWidget {
text: _styledTextSpan, text: _styledTextSpan,
cursorColor: cursorColor, cursorColor: cursorColor,
showCursor: showCursor, showCursor: showCursor,
multiline: multiline,
selectionColor: selectionColor, selectionColor: selectionColor,
textScaleFactor: textScaleFactor, textScaleFactor: textScaleFactor,
selection: value.selection, selection: value.selection,
......
...@@ -39,15 +39,6 @@ enum TextSelectionHandleType { ...@@ -39,15 +39,6 @@ enum TextSelectionHandleType {
collapsed, collapsed,
} }
/// Builds a selection handle of the given type.
typedef Widget TextSelectionHandleBuilder(BuildContext context, TextSelectionHandleType type);
/// Builds a toolbar near a text selection.
///
/// Typically displays buttons for copying and pasting text.
// TODO(mpcomplete): A single position is probably insufficient.
typedef Widget TextSelectionToolbarBuilder(BuildContext context, Point position, TextSelectionDelegate delegate);
/// The text position that a give selection handle manipulates. Dragging the /// The text position that a give selection handle manipulates. Dragging the
/// [start] handle always moves the [start]/[baseOffset] of the selection. /// [start] handle always moves the [start]/[baseOffset] of the selection.
enum _TextSelectionHandlePosition { start, end } enum _TextSelectionHandlePosition { start, end }
...@@ -65,6 +56,22 @@ abstract class TextSelectionDelegate { ...@@ -65,6 +56,22 @@ abstract class TextSelectionDelegate {
void hideToolbar(); void hideToolbar();
} }
// An interface for building the selection UI, to be provided by the
// implementor of the toolbar widget.
abstract class TextSelectionControls {
/// Builds a selection handle of the given type.
Widget buildHandle(BuildContext context, TextSelectionHandleType type);
/// Builds a toolbar near a text selection.
///
/// Typically displays buttons for copying and pasting text.
// TODO(mpcomplete): A single position is probably insufficient.
Widget buildToolbar(BuildContext context, Point position, TextSelectionDelegate delegate);
/// Returns the size of the selection handle.
Size get handleSize;
}
/// An object that manages a pair of text selection handles. /// An object that manages a pair of text selection handles.
/// ///
/// The selection handles are displayed in the [Overlay] that most closely /// The selection handles are displayed in the [Overlay] that most closely
...@@ -79,8 +86,7 @@ class TextSelectionOverlay implements TextSelectionDelegate { ...@@ -79,8 +86,7 @@ class TextSelectionOverlay implements TextSelectionDelegate {
this.debugRequiredFor, this.debugRequiredFor,
this.renderObject, this.renderObject,
this.onSelectionOverlayChanged, this.onSelectionOverlayChanged,
this.handleBuilder, this.selectionControls,
this.toolbarBuilder
}): _input = input { }): _input = input {
assert(context != null); assert(context != null);
final OverlayState overlay = Overlay.of(context); final OverlayState overlay = Overlay.of(context);
...@@ -109,16 +115,8 @@ class TextSelectionOverlay implements TextSelectionDelegate { ...@@ -109,16 +115,8 @@ class TextSelectionOverlay implements TextSelectionDelegate {
/// will be called with a new input value with an updated selection. /// will be called with a new input value with an updated selection.
final ValueChanged<InputValue> onSelectionOverlayChanged; final ValueChanged<InputValue> onSelectionOverlayChanged;
/// Builds the selection handles. /// Builds text selection handles and toolbar.
/// final TextSelectionControls selectionControls;
/// The selection handles let the user adjust which portion of the text is
/// selected.
final TextSelectionHandleBuilder handleBuilder;
/// Builds a toolbar to display near the selection.
///
/// The toolbar typically contains buttons for copying and pasting text.
final TextSelectionToolbarBuilder toolbarBuilder;
/// Controls the fade-in animations. /// Controls the fade-in animations.
static const Duration _kFadeDuration = const Duration(milliseconds: 150); static const Duration _kFadeDuration = const Duration(milliseconds: 150);
...@@ -208,7 +206,7 @@ class TextSelectionOverlay implements TextSelectionDelegate { ...@@ -208,7 +206,7 @@ class TextSelectionOverlay implements TextSelectionDelegate {
Widget _buildHandle(BuildContext context, _TextSelectionHandlePosition position) { Widget _buildHandle(BuildContext context, _TextSelectionHandlePosition position) {
if ((_selection.isCollapsed && position == _TextSelectionHandlePosition.end) || if ((_selection.isCollapsed && position == _TextSelectionHandlePosition.end) ||
handleBuilder == null) selectionControls == null)
return new Container(); // hide the second handle when collapsed return new Container(); // hide the second handle when collapsed
return new FadeTransition( return new FadeTransition(
...@@ -218,14 +216,14 @@ class TextSelectionOverlay implements TextSelectionDelegate { ...@@ -218,14 +216,14 @@ class TextSelectionOverlay implements TextSelectionDelegate {
onSelectionHandleTapped: _handleSelectionHandleTapped, onSelectionHandleTapped: _handleSelectionHandleTapped,
renderObject: renderObject, renderObject: renderObject,
selection: _selection, selection: _selection,
builder: handleBuilder, selectionControls: selectionControls,
position: position position: position
) )
); );
} }
Widget _buildToolbar(BuildContext context) { Widget _buildToolbar(BuildContext context) {
if (toolbarBuilder == null) if (selectionControls == null)
return new Container(); return new Container();
// Find the horizontal midpoint, just above the selected text. // Find the horizontal midpoint, just above the selected text.
...@@ -239,7 +237,7 @@ class TextSelectionOverlay implements TextSelectionDelegate { ...@@ -239,7 +237,7 @@ class TextSelectionOverlay implements TextSelectionDelegate {
return new FadeTransition( return new FadeTransition(
opacity: _toolbarOpacity, opacity: _toolbarOpacity,
child: toolbarBuilder(context, midpoint, this) child: selectionControls.buildToolbar(context, midpoint, this)
); );
} }
...@@ -283,7 +281,7 @@ class _TextSelectionHandleOverlay extends StatefulWidget { ...@@ -283,7 +281,7 @@ class _TextSelectionHandleOverlay extends StatefulWidget {
this.renderObject, this.renderObject,
this.onSelectionHandleChanged, this.onSelectionHandleChanged,
this.onSelectionHandleTapped, this.onSelectionHandleTapped,
this.builder this.selectionControls
}) : super(key: key); }) : super(key: key);
final TextSelection selection; final TextSelection selection;
...@@ -291,7 +289,7 @@ class _TextSelectionHandleOverlay extends StatefulWidget { ...@@ -291,7 +289,7 @@ class _TextSelectionHandleOverlay extends StatefulWidget {
final RenderEditableLine renderObject; final RenderEditableLine renderObject;
final ValueChanged<TextSelection> onSelectionHandleChanged; final ValueChanged<TextSelection> onSelectionHandleChanged;
final VoidCallback onSelectionHandleTapped; final VoidCallback onSelectionHandleTapped;
final TextSelectionHandleBuilder builder; final TextSelectionControls selectionControls;
@override @override
_TextSelectionHandleOverlayState createState() => new _TextSelectionHandleOverlayState(); _TextSelectionHandleOverlayState createState() => new _TextSelectionHandleOverlayState();
...@@ -301,7 +299,7 @@ class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay ...@@ -301,7 +299,7 @@ class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay
Point _dragPosition; Point _dragPosition;
void _handleDragStart(DragStartDetails details) { void _handleDragStart(DragStartDetails details) {
_dragPosition = details.globalPosition; _dragPosition = details.globalPosition + new Offset(0.0, -config.selectionControls.handleSize.height);
} }
void _handleDragUpdate(DragUpdateDetails details) { void _handleDragUpdate(DragUpdateDetails details) {
...@@ -360,15 +358,15 @@ class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay ...@@ -360,15 +358,15 @@ class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay
} }
return new GestureDetector( return new GestureDetector(
onHorizontalDragStart: _handleDragStart, onPanStart: _handleDragStart,
onHorizontalDragUpdate: _handleDragUpdate, onPanUpdate: _handleDragUpdate,
onTap: _handleTap, onTap: _handleTap,
child: new Stack( child: new Stack(
children: <Widget>[ children: <Widget>[
new Positioned( new Positioned(
left: point.x, left: point.x,
top: point.y, top: point.y,
child: config.builder(context, type) child: config.selectionControls.buildHandle(context, type)
) )
] ]
) )
......
...@@ -436,4 +436,132 @@ void main() { ...@@ -436,4 +436,132 @@ void main() {
// End the test here to ensure the animation is properly disposed of. // End the test here to ensure the animation is properly disposed of.
}); });
testWidgets('Multiline text will wrap', (WidgetTester tester) async {
GlobalKey inputKey = new GlobalKey();
InputValue inputValue = InputValue.empty;
Widget builder() {
return new Center(
child: new Material(
child: new Input(
value: inputValue,
key: inputKey,
style: const TextStyle(color: Colors.black, fontSize: 34.0),
multiline: true,
hintText: 'Placeholder',
onChanged: (InputValue value) { inputValue = value; }
)
)
);
}
await tester.pumpWidget(builder());
RenderBox findInputBox() => tester.renderObject(find.byKey(inputKey));
RenderBox inputBox = findInputBox();
Size emptyInputSize = inputBox.size;
enterText('This is a long line of text that will wrap to multiple lines.');
await tester.pumpWidget(builder());
expect(findInputBox(), equals(inputBox));
expect(inputBox.size, greaterThan(emptyInputSize));
enterText('No wrapping here.');
await tester.pumpWidget(builder());
expect(findInputBox(), equals(inputBox));
expect(inputBox.size, equals(emptyInputSize));
});
testWidgets('Can drag handles to change selection in multiline', (WidgetTester tester) async {
GlobalKey inputKey = new GlobalKey();
InputValue inputValue = InputValue.empty;
Widget builder() {
return new Overlay(
initialEntries: <OverlayEntry>[
new OverlayEntry(
builder: (BuildContext context) {
return new Center(
child: new Material(
child: new Input(
value: inputValue,
key: inputKey,
style: const TextStyle(color: Colors.black, fontSize: 34.0),
multiline: true,
onChanged: (InputValue value) { inputValue = value; }
)
)
);
}
)
]
);
}
await tester.pumpWidget(builder());
String testValue = 'First line of text is here abcdef ghijkl mnopqrst. Second line of text goes until abcdef ghijkl mnopq. Third line of stuff.';
String cutValue = 'First line of stuff.';
enterText(testValue);
await tester.pumpWidget(builder());
// Check that the text spans multiple lines.
Point firstPos = textOffsetToPosition(tester, testValue.indexOf('First'));
Point secondPos = textOffsetToPosition(tester, testValue.indexOf('Second'));
Point thirdPos = textOffsetToPosition(tester, testValue.indexOf('Third'));
expect(firstPos.x, secondPos.x);
expect(firstPos.x, thirdPos.x);
expect(firstPos.y, lessThan(secondPos.y));
expect(secondPos.y, lessThan(thirdPos.y));
// Long press the 'n' in 'until' to select the word.
Point untilPos = textOffsetToPosition(tester, testValue.indexOf('until')+1);
TestGesture gesture = await tester.startGesture(untilPos, pointer: 7);
await tester.pump(const Duration(seconds: 2));
await gesture.up();
await tester.pump();
expect(inputValue.selection.baseOffset, 76);
expect(inputValue.selection.extentOffset, 81);
RenderEditableLine renderLine = findRenderEditableLine(tester);
List<TextSelectionPoint> endpoints = renderLine.getEndpointsForSelection(
inputValue.selection);
expect(endpoints.length, 2);
// Drag the right handle to the third line, just after 'Third'.
Point handlePos = endpoints[1].point + new Offset(1.0, 1.0);
Point newHandlePos = textOffsetToPosition(tester, testValue.indexOf('Third') + 5);
gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(inputValue.selection.baseOffset, 76);
expect(inputValue.selection.extentOffset, 108);
// Drag the left handle to the first line, just after 'First'.
handlePos = endpoints[0].point + new Offset(-1.0, 1.0);
newHandlePos = textOffsetToPosition(tester, testValue.indexOf('First') + 5);
gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pumpWidget(builder());
expect(inputValue.selection.baseOffset, 5);
expect(inputValue.selection.extentOffset, 108);
await tester.tap(find.text('CUT'));
await tester.pumpWidget(builder());
expect(inputValue.selection.isCollapsed, true);
expect(inputValue.text, cutValue);
});
} }
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