Add multiline support to Input and friends. (#6155)
Fixes https://github.com/flutter/flutter/issues/6154
This commit is contained in:
@@ -94,6 +94,12 @@ class TextFieldDemoState extends State<TextFieldDemo> {
|
||||
validator: _validatePhoneNumber
|
||||
)
|
||||
),
|
||||
new Input(
|
||||
hintText: 'Tell us about yourself (optional)',
|
||||
labelText: 'Life story',
|
||||
multiline: true,
|
||||
formField: new FormField<String>()
|
||||
),
|
||||
new Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
|
||||
@@ -43,6 +43,7 @@ class Input extends StatefulWidget {
|
||||
this.hideText: false,
|
||||
this.isDense: false,
|
||||
this.autofocus: false,
|
||||
this.multiline: false,
|
||||
this.formField,
|
||||
this.onChanged,
|
||||
this.onSubmitted
|
||||
@@ -84,6 +85,10 @@ class Input extends StatefulWidget {
|
||||
/// Whether this input field should focus itself is nothing else is already focused.
|
||||
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.
|
||||
final FormField<String> formField;
|
||||
|
||||
@@ -205,10 +210,10 @@ class _InputState extends State<Input> {
|
||||
focusKey: focusKey,
|
||||
style: textStyle,
|
||||
hideText: config.hideText,
|
||||
multiline: config.multiline,
|
||||
cursorColor: themeData.textSelectionColor,
|
||||
selectionColor: themeData.textSelectionColor,
|
||||
selectionHandleBuilder: buildTextSelectionHandle,
|
||||
selectionToolbarBuilder: buildTextSelectionToolbar,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
platform: Theme.of(context).platform,
|
||||
keyboardType: config.keyboardType,
|
||||
onChanged: onChanged,
|
||||
|
||||
@@ -143,49 +143,57 @@ class _TextSelectionHandlePainter extends CustomPainter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for material-style copy/paste text selection toolbar.
|
||||
Widget buildTextSelectionToolbar(
|
||||
BuildContext context, Point position, TextSelectionDelegate delegate) {
|
||||
final Size screenSize = MediaQuery.of(context).size;
|
||||
return new ConstrainedBox(
|
||||
constraints: new BoxConstraints.loose(screenSize),
|
||||
child: new CustomSingleChildLayout(
|
||||
delegate: new _TextSelectionToolbarLayout(position),
|
||||
child: new _TextSelectionToolbar(delegate)
|
||||
)
|
||||
);
|
||||
}
|
||||
class _MaterialTextSelectionControls extends TextSelectionControls {
|
||||
@override
|
||||
Size handleSize = const Size(_kHandleSize, _kHandleSize);
|
||||
|
||||
/// Builder for material-style text selection handles.
|
||||
Widget buildTextSelectionHandle(
|
||||
BuildContext context, TextSelectionHandleType type) {
|
||||
Widget handle = new SizedBox(
|
||||
width: _kHandleSize,
|
||||
height: _kHandleSize,
|
||||
child: new CustomPaint(
|
||||
painter: new _TextSelectionHandlePainter(
|
||||
color: Theme.of(context).textSelectionHandleColor
|
||||
/// Builder for material-style copy/paste text selection toolbar.
|
||||
@override
|
||||
Widget buildToolbar(
|
||||
BuildContext context, Point position, TextSelectionDelegate delegate) {
|
||||
final Size screenSize = MediaQuery.of(context).size;
|
||||
return new ConstrainedBox(
|
||||
constraints: new BoxConstraints.loose(screenSize),
|
||||
child: new CustomSingleChildLayout(
|
||||
delegate: new _TextSelectionToolbarLayout(position),
|
||||
child: new _TextSelectionToolbar(delegate)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// [handle] is a circle, with a rectangle in the top left quadrant of that
|
||||
// circle (an onion pointing to 10:30). We rotate [handle] to point
|
||||
// straight up or up-right depending on the handle type.
|
||||
switch (type) {
|
||||
case TextSelectionHandleType.left: // points up-right
|
||||
return new Transform(
|
||||
transform: new Matrix4.rotationZ(math.PI / 2.0),
|
||||
child: handle
|
||||
);
|
||||
case TextSelectionHandleType.right: // points up-left
|
||||
return handle;
|
||||
case TextSelectionHandleType.collapsed: // points up
|
||||
return new Transform(
|
||||
transform: new Matrix4.rotationZ(math.PI / 4.0),
|
||||
child: handle
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
/// Builder for material-style text selection handles.
|
||||
@override
|
||||
Widget buildHandle(BuildContext context, TextSelectionHandleType type) {
|
||||
Widget handle = new SizedBox(
|
||||
width: _kHandleSize,
|
||||
height: _kHandleSize,
|
||||
child: new CustomPaint(
|
||||
painter: new _TextSelectionHandlePainter(
|
||||
color: Theme.of(context).textSelectionHandleColor
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// [handle] is a circle, with a rectangle in the top left quadrant of that
|
||||
// circle (an onion pointing to 10:30). We rotate [handle] to point
|
||||
// straight up or up-right depending on the handle type.
|
||||
switch (type) {
|
||||
case TextSelectionHandleType.left: // points up-right
|
||||
return new Transform(
|
||||
transform: new Matrix4.rotationZ(math.PI / 2.0),
|
||||
child: handle
|
||||
);
|
||||
case TextSelectionHandleType.right: // points up-left
|
||||
return handle;
|
||||
case TextSelectionHandleType.collapsed: // points up
|
||||
return new Transform(
|
||||
transform: new Matrix4.rotationZ(math.PI / 4.0),
|
||||
child: handle
|
||||
);
|
||||
}
|
||||
assert(type != null);
|
||||
return null;
|
||||
}
|
||||
assert(type != null);
|
||||
return null;
|
||||
}
|
||||
|
||||
final _MaterialTextSelectionControls materialTextSelectionControls = new _MaterialTextSelectionControls();
|
||||
|
||||
@@ -219,7 +219,7 @@ class TextPainter {
|
||||
ui.TextBox box = boxes[0];
|
||||
double caretEnd = box.end;
|
||||
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) {
|
||||
@@ -229,7 +229,7 @@ class TextPainter {
|
||||
ui.TextBox box = boxes[0];
|
||||
double caretStart = box.start;
|
||||
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.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:ui' as ui show Paragraph, ParagraphBuilder, ParagraphConstraints, ParagraphStyle, TextBox;
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
|
||||
@@ -43,15 +44,17 @@ class RenderEditableLine extends RenderBox {
|
||||
TextSpan text,
|
||||
Color cursorColor,
|
||||
bool showCursor: false,
|
||||
bool multiline: false,
|
||||
Color selectionColor,
|
||||
double textScaleFactor: 1.0,
|
||||
TextSelection selection,
|
||||
this.onSelectionChanged,
|
||||
Offset paintOffset: Offset.zero,
|
||||
this.onPaintOffsetUpdateNeeded
|
||||
this.onPaintOffsetUpdateNeeded,
|
||||
}) : _textPainter = new TextPainter(text: text, textScaleFactor: textScaleFactor),
|
||||
_cursorColor = cursorColor,
|
||||
_showCursor = showCursor,
|
||||
_multiline = multiline,
|
||||
_selection = selection,
|
||||
_paintOffset = paintOffset {
|
||||
assert(!showCursor || cursorColor != null);
|
||||
@@ -163,14 +166,14 @@ class RenderEditableLine extends RenderBox {
|
||||
// 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
|
||||
// clean at this point.
|
||||
_textPainter.layout();
|
||||
_textPainter.layout(maxWidth: _maxContentWidth);
|
||||
|
||||
Offset offset = _paintOffset;
|
||||
|
||||
if (selection.isCollapsed) {
|
||||
// TODO(mpcomplete): This doesn't work well at an RTL/LTR boundary.
|
||||
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)];
|
||||
} else {
|
||||
List<ui.TextBox> boxes = _textPainter.getBoxesForSelection(selection);
|
||||
@@ -200,11 +203,18 @@ class RenderEditableLine extends RenderBox {
|
||||
// TODO(abarth): ParagraphBuilder#build's argument should be optional.
|
||||
// TODO(abarth): These min/max values should be the default for ui.Paragraph.
|
||||
_layoutTemplate = builder.build(new ui.ParagraphStyle())
|
||||
..layout(new ui.ParagraphConstraints(width: double.INFINITY));
|
||||
..layout(new ui.ParagraphConstraints(width: _maxContentWidth));
|
||||
}
|
||||
return _layoutTemplate.height;
|
||||
}
|
||||
|
||||
bool _multiline;
|
||||
double get _maxContentWidth {
|
||||
return _multiline ?
|
||||
constraints.maxWidth - (_kCaretGap + _kCaretWidth) :
|
||||
double.INFINITY;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicHeight(double width) {
|
||||
return _preferredHeight;
|
||||
@@ -274,10 +284,11 @@ class RenderEditableLine extends RenderBox {
|
||||
@override
|
||||
void performLayout() {
|
||||
Size oldSize = hasSize ? size : null;
|
||||
size = new Size(constraints.maxWidth, constraints.constrainHeight(_preferredHeight));
|
||||
_caretPrototype = new Rect.fromLTWH(0.0, _kCaretHeightOffset, _kCaretWidth, size.height - 2.0 * _kCaretHeightOffset);
|
||||
double lineHeight = constraints.constrainHeight(_preferredHeight);
|
||||
_caretPrototype = new Rect.fromLTWH(0.0, _kCaretHeightOffset, _kCaretWidth, lineHeight - 2.0 * _kCaretHeightOffset);
|
||||
_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);
|
||||
if (onPaintOffsetUpdateNeeded != null && (size != oldSize || contentSize != _contentSize))
|
||||
onPaintOffsetUpdateNeeded(new ViewportDimensions(containerSize: size, contentSize: contentSize));
|
||||
|
||||
@@ -156,6 +156,8 @@ class InputValue {
|
||||
///
|
||||
/// This control is not intended to be used directly. Instead, consider using
|
||||
/// [Input], which provides focus management and material design.
|
||||
//
|
||||
// TODO(mpcomplete): rename RawInput since it can span multiple lines.
|
||||
class RawInputLine extends Scrollable {
|
||||
/// Creates a basic single-line input control.
|
||||
///
|
||||
@@ -168,9 +170,9 @@ class RawInputLine extends Scrollable {
|
||||
this.style,
|
||||
this.cursorColor,
|
||||
this.textScaleFactor,
|
||||
this.multiline,
|
||||
this.selectionColor,
|
||||
this.selectionHandleBuilder,
|
||||
this.selectionToolbarBuilder,
|
||||
this.selectionControls,
|
||||
@required this.platform,
|
||||
this.keyboardType,
|
||||
this.onChanged,
|
||||
@@ -206,16 +208,15 @@ class RawInputLine extends Scrollable {
|
||||
/// The color to use when painting the cursor.
|
||||
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.
|
||||
final Color selectionColor;
|
||||
|
||||
/// Optional builder function for a widget that controls the boundary of a
|
||||
/// text selection.
|
||||
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;
|
||||
/// Optional delegate for building the text selection handles and toolbar.
|
||||
final TextSelectionControls selectionControls;
|
||||
|
||||
/// The platform whose behavior should be approximated, in particular
|
||||
/// for scroll physics. (See [ScrollBehavior.platform].)
|
||||
@@ -356,15 +357,14 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
|
||||
_selectionOverlay = null;
|
||||
}
|
||||
|
||||
if (config.selectionHandleBuilder != null) {
|
||||
if (config.selectionControls != null) {
|
||||
_selectionOverlay = new TextSelectionOverlay(
|
||||
input: newInput,
|
||||
context: context,
|
||||
debugRequiredFor: config,
|
||||
renderObject: renderObject,
|
||||
onSelectionOverlayChanged: _handleSelectionOverlayChanged,
|
||||
handleBuilder: config.selectionHandleBuilder,
|
||||
toolbarBuilder: config.selectionToolbarBuilder
|
||||
selectionControls: config.selectionControls,
|
||||
);
|
||||
if (newInput.text.isNotEmpty || longPress)
|
||||
_selectionOverlay.showHandles();
|
||||
@@ -443,6 +443,7 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
|
||||
style: config.style,
|
||||
cursorColor: config.cursorColor,
|
||||
showCursor: _showCursor,
|
||||
multiline: config.multiline,
|
||||
selectionColor: config.selectionColor,
|
||||
textScaleFactor: config.textScaleFactor ?? MediaQuery.of(context).textScaleFactor,
|
||||
hideText: config.hideText,
|
||||
@@ -460,6 +461,7 @@ class _EditableLineWidget extends LeafRenderObjectWidget {
|
||||
this.style,
|
||||
this.cursorColor,
|
||||
this.showCursor,
|
||||
this.multiline,
|
||||
this.selectionColor,
|
||||
this.textScaleFactor,
|
||||
this.hideText,
|
||||
@@ -472,6 +474,7 @@ class _EditableLineWidget extends LeafRenderObjectWidget {
|
||||
final TextStyle style;
|
||||
final Color cursorColor;
|
||||
final bool showCursor;
|
||||
final bool multiline;
|
||||
final Color selectionColor;
|
||||
final double textScaleFactor;
|
||||
final bool hideText;
|
||||
@@ -485,6 +488,7 @@ class _EditableLineWidget extends LeafRenderObjectWidget {
|
||||
text: _styledTextSpan,
|
||||
cursorColor: cursorColor,
|
||||
showCursor: showCursor,
|
||||
multiline: multiline,
|
||||
selectionColor: selectionColor,
|
||||
textScaleFactor: textScaleFactor,
|
||||
selection: value.selection,
|
||||
|
||||
@@ -39,15 +39,6 @@ enum TextSelectionHandleType {
|
||||
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
|
||||
/// [start] handle always moves the [start]/[baseOffset] of the selection.
|
||||
enum _TextSelectionHandlePosition { start, end }
|
||||
@@ -65,6 +56,22 @@ abstract class TextSelectionDelegate {
|
||||
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.
|
||||
///
|
||||
/// The selection handles are displayed in the [Overlay] that most closely
|
||||
@@ -79,8 +86,7 @@ class TextSelectionOverlay implements TextSelectionDelegate {
|
||||
this.debugRequiredFor,
|
||||
this.renderObject,
|
||||
this.onSelectionOverlayChanged,
|
||||
this.handleBuilder,
|
||||
this.toolbarBuilder
|
||||
this.selectionControls,
|
||||
}): _input = input {
|
||||
assert(context != null);
|
||||
final OverlayState overlay = Overlay.of(context);
|
||||
@@ -109,16 +115,8 @@ class TextSelectionOverlay implements TextSelectionDelegate {
|
||||
/// will be called with a new input value with an updated selection.
|
||||
final ValueChanged<InputValue> onSelectionOverlayChanged;
|
||||
|
||||
/// Builds the selection handles.
|
||||
///
|
||||
/// 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;
|
||||
/// Builds text selection handles and toolbar.
|
||||
final TextSelectionControls selectionControls;
|
||||
|
||||
/// Controls the fade-in animations.
|
||||
static const Duration _kFadeDuration = const Duration(milliseconds: 150);
|
||||
@@ -208,7 +206,7 @@ class TextSelectionOverlay implements TextSelectionDelegate {
|
||||
|
||||
Widget _buildHandle(BuildContext context, _TextSelectionHandlePosition position) {
|
||||
if ((_selection.isCollapsed && position == _TextSelectionHandlePosition.end) ||
|
||||
handleBuilder == null)
|
||||
selectionControls == null)
|
||||
return new Container(); // hide the second handle when collapsed
|
||||
|
||||
return new FadeTransition(
|
||||
@@ -218,14 +216,14 @@ class TextSelectionOverlay implements TextSelectionDelegate {
|
||||
onSelectionHandleTapped: _handleSelectionHandleTapped,
|
||||
renderObject: renderObject,
|
||||
selection: _selection,
|
||||
builder: handleBuilder,
|
||||
selectionControls: selectionControls,
|
||||
position: position
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildToolbar(BuildContext context) {
|
||||
if (toolbarBuilder == null)
|
||||
if (selectionControls == null)
|
||||
return new Container();
|
||||
|
||||
// Find the horizontal midpoint, just above the selected text.
|
||||
@@ -239,7 +237,7 @@ class TextSelectionOverlay implements TextSelectionDelegate {
|
||||
|
||||
return new FadeTransition(
|
||||
opacity: _toolbarOpacity,
|
||||
child: toolbarBuilder(context, midpoint, this)
|
||||
child: selectionControls.buildToolbar(context, midpoint, this)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -283,7 +281,7 @@ class _TextSelectionHandleOverlay extends StatefulWidget {
|
||||
this.renderObject,
|
||||
this.onSelectionHandleChanged,
|
||||
this.onSelectionHandleTapped,
|
||||
this.builder
|
||||
this.selectionControls
|
||||
}) : super(key: key);
|
||||
|
||||
final TextSelection selection;
|
||||
@@ -291,7 +289,7 @@ class _TextSelectionHandleOverlay extends StatefulWidget {
|
||||
final RenderEditableLine renderObject;
|
||||
final ValueChanged<TextSelection> onSelectionHandleChanged;
|
||||
final VoidCallback onSelectionHandleTapped;
|
||||
final TextSelectionHandleBuilder builder;
|
||||
final TextSelectionControls selectionControls;
|
||||
|
||||
@override
|
||||
_TextSelectionHandleOverlayState createState() => new _TextSelectionHandleOverlayState();
|
||||
@@ -301,7 +299,7 @@ class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay
|
||||
Point _dragPosition;
|
||||
|
||||
void _handleDragStart(DragStartDetails details) {
|
||||
_dragPosition = details.globalPosition;
|
||||
_dragPosition = details.globalPosition + new Offset(0.0, -config.selectionControls.handleSize.height);
|
||||
}
|
||||
|
||||
void _handleDragUpdate(DragUpdateDetails details) {
|
||||
@@ -360,15 +358,15 @@ class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay
|
||||
}
|
||||
|
||||
return new GestureDetector(
|
||||
onHorizontalDragStart: _handleDragStart,
|
||||
onHorizontalDragUpdate: _handleDragUpdate,
|
||||
onPanStart: _handleDragStart,
|
||||
onPanUpdate: _handleDragUpdate,
|
||||
onTap: _handleTap,
|
||||
child: new Stack(
|
||||
children: <Widget>[
|
||||
new Positioned(
|
||||
left: point.x,
|
||||
top: point.y,
|
||||
child: config.builder(context, type)
|
||||
child: config.selectionControls.buildHandle(context, type)
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
@@ -436,4 +436,132 @@ void main() {
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user