Commit a5920c3d authored by Matt Perry's avatar Matt Perry

First pass at a material-style copy/paste toolbar. (#3698)

* First pass at a material-style copy/paste toolbar.

This mimics the toolbar you see when selecting text in an Android
material app. There's still more to do (like integration with the system
clipboard), but this seemed like a good stopping point.

BUG=https://github.com/flutter/flutter/issues/1567
parent f34d3171
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -11,12 +9,12 @@ import 'colors.dart'; ...@@ -11,12 +9,12 @@ import 'colors.dart';
import 'debug.dart'; import 'debug.dart';
import 'icon.dart'; import 'icon.dart';
import 'icons.dart'; import 'icons.dart';
import 'material.dart';
import 'text_selection.dart';
import 'theme.dart'; import 'theme.dart';
export 'package:sky_services/editing/editing.mojom.dart' show KeyboardType; export 'package:sky_services/editing/editing.mojom.dart' show KeyboardType;
const double _kTextSelectionHandleSize = 20.0; // pixels
/// A material design text input field. /// A material design text input field.
/// ///
/// Requires one of its ancestors to be a [Material] widget. /// Requires one of its ancestors to be a [Material] widget.
...@@ -198,7 +196,8 @@ class _InputState extends State<Input> { ...@@ -198,7 +196,8 @@ class _InputState extends State<Input> {
hideText: config.hideText, hideText: config.hideText,
cursorColor: themeData.textSelectionColor, cursorColor: themeData.textSelectionColor,
selectionColor: themeData.textSelectionColor, selectionColor: themeData.textSelectionColor,
selectionHandleBuilder: _textSelectionHandleBuilder, selectionHandleBuilder: buildTextSelectionHandle,
selectionToolbarBuilder: buildTextSelectionToolbar,
keyboardType: config.keyboardType, keyboardType: config.keyboardType,
onChanged: onChanged, onChanged: onChanged,
onSubmitted: onSubmitted onSubmitted: onSubmitted
...@@ -245,37 +244,6 @@ class _InputState extends State<Input> { ...@@ -245,37 +244,6 @@ class _InputState extends State<Input> {
) )
); );
} }
Widget _textSelectionHandleBuilder(
BuildContext context, TextSelectionHandleType type) {
Widget handle = new SizedBox(
width: _kTextSelectionHandleSize,
height: _kTextSelectionHandleSize,
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.identity().rotateZ(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.identity().rotateZ(math.PI / 4.0),
child: handle
);
}
}
} }
class _FormFieldData { class _FormFieldData {
...@@ -310,24 +278,3 @@ class _FormFieldData { ...@@ -310,24 +278,3 @@ class _FormFieldData {
scope.onFieldChanged(); scope.onFieldChanged();
} }
} }
/// Draws a single text selection handle. The [type] determines where the handle
/// points (e.g. the [left] handle points up and to the right).
class _TextSelectionHandlePainter extends CustomPainter {
_TextSelectionHandlePainter({ this.color });
final Color color;
@override
void paint(Canvas canvas, Size size) {
Paint paint = new Paint()..color = color;
double radius = size.width/2.0;
canvas.drawCircle(new Point(radius, radius), radius, paint);
canvas.drawRect(new Rect.fromLTWH(0.0, 0.0, radius, radius), paint);
}
@override
bool shouldRepaint(_TextSelectionHandlePainter oldPainter) {
return color != oldPainter.color;
}
}
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/widgets.dart';
import 'flat_button.dart';
import 'icon_button.dart';
import 'icons.dart';
import 'material.dart';
import 'theme.dart';
const double _kHandleSize = 22.0; // pixels
const double _kToolbarScreenPadding = 8.0; // pixels
/// Manages a copy/paste text selection toolbar.
class _TextSelectionToolbar extends StatelessWidget {
_TextSelectionToolbar(this.delegate, {Key key}) : super(key: key);
final TextSelectionDelegate delegate;
InputValue get value => delegate.inputValue;
@override
Widget build(BuildContext context) {
List<Widget> items = <Widget>[];
if (!value.selection.isCollapsed) {
items.add(new FlatButton(child: new Text('CUT'), onPressed: _handleCut));
items.add(new FlatButton(child: new Text('COPY'), onPressed: _handleCopy));
}
items.add(new FlatButton(
child: new Text('PASTE'),
onPressed: delegate.pasteBuffer != null ? _handlePaste : null)
);
if (value.selection.isCollapsed) {
items.add(new FlatButton(child: new Text('SELECT ALL'), onPressed: _handleSelectAll));
}
// TODO(mpcomplete): implement `more` menu.
items.add(new IconButton(icon: Icons.more_vert));
return new Material(
elevation: 1,
child: new Container(
height: 44.0,
child: new Row(mainAxisAlignment: MainAxisAlignment.collapse, children: items)
)
);
}
void _handleCut() {
InputValue value = this.value;
delegate.pasteBuffer = value.selection.textInside(value.text);
delegate.inputValue = new InputValue(
text: value.selection.textBefore(value.text) + value.selection.textAfter(value.text),
selection: new TextSelection.collapsed(offset: value.selection.start)
);
delegate.hideToolbar();
}
void _handleCopy() {
delegate.pasteBuffer = value.selection.textInside(value.text);
delegate.inputValue = new InputValue(
text: value.text,
selection: new TextSelection.collapsed(offset: value.selection.end)
);
delegate.hideToolbar();
}
void _handlePaste() {
delegate.inputValue = new InputValue(
text: value.selection.textBefore(value.text) + delegate.pasteBuffer + value.selection.textAfter(value.text),
selection: new TextSelection.collapsed(offset: value.selection.start + delegate.pasteBuffer.length)
);
delegate.hideToolbar();
}
void _handleSelectAll() {
delegate.inputValue = new InputValue(
text: value.text,
selection: new TextSelection(baseOffset: 0, extentOffset: value.text.length)
);
}
}
/// Centers the toolbar around the given position, ensuring that it remains on
/// screen.
class _TextSelectionToolbarLayout extends SingleChildLayoutDelegate {
_TextSelectionToolbarLayout(this.position);
final Point position;
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
return constraints.loosen();
}
@override
Offset getPositionForChild(Size size, Size childSize) {
double x = position.x - childSize.width/2.0;
double y = position.y - childSize.height;
if (x < _kToolbarScreenPadding)
x = _kToolbarScreenPadding;
else if (x + childSize.width > size.width - 2 * _kToolbarScreenPadding)
x = size.width - childSize.width - _kToolbarScreenPadding;
if (y < _kToolbarScreenPadding)
y = _kToolbarScreenPadding;
else if (y + childSize.height > size.height - 2 * _kToolbarScreenPadding)
y = size.height - childSize.height - _kToolbarScreenPadding;
return new Offset(x, y);
}
@override
bool shouldRelayout(_TextSelectionToolbarLayout oldDelegate) {
return position != oldDelegate.position;
}
}
/// Draws a single text selection handle. The [type] determines where the handle
/// points (e.g. the [left] handle points up and to the right).
class _TextSelectionHandlePainter extends CustomPainter {
_TextSelectionHandlePainter({ this.color });
final Color color;
@override
void paint(Canvas canvas, Size size) {
Paint paint = new Paint()..color = color;
double radius = size.width/2.0;
canvas.drawCircle(new Point(radius, radius), radius, paint);
canvas.drawRect(new Rect.fromLTWH(0.0, 0.0, radius, radius), paint);
}
@override
bool shouldRepaint(_TextSelectionHandlePainter oldPainter) {
return color != oldPainter.color;
}
}
/// 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)
)
);
}
/// 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
)
)
);
// [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.identity().rotateZ(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.identity().rotateZ(math.PI / 4.0),
child: handle
);
}
}
...@@ -154,6 +154,7 @@ class RawInputLine extends Scrollable { ...@@ -154,6 +154,7 @@ class RawInputLine extends Scrollable {
this.cursorColor, this.cursorColor,
this.selectionColor, this.selectionColor,
this.selectionHandleBuilder, this.selectionHandleBuilder,
this.selectionToolbarBuilder,
this.keyboardType, this.keyboardType,
this.onChanged, this.onChanged,
this.onSubmitted this.onSubmitted
...@@ -181,8 +182,14 @@ class RawInputLine extends Scrollable { ...@@ -181,8 +182,14 @@ class RawInputLine extends Scrollable {
/// 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
/// text selection.
final TextSelectionHandleBuilder selectionHandleBuilder; 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 type of keyboard to use for editing the text. /// The type of keyboard to use for editing the text.
final KeyboardType keyboardType; final KeyboardType keyboardType;
...@@ -202,7 +209,7 @@ class RawInputLineState extends ScrollableState<RawInputLine> { ...@@ -202,7 +209,7 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
_KeyboardClientImpl _keyboardClient; _KeyboardClientImpl _keyboardClient;
KeyboardHandle _keyboardHandle; KeyboardHandle _keyboardHandle;
TextSelectionHandles _selectionHandles; TextSelectionOverlay _selectionOverlay;
@override @override
ScrollBehavior<double, double> createScrollBehavior() => new BoundedBehavior(); ScrollBehavior<double, double> createScrollBehavior() => new BoundedBehavior();
...@@ -229,12 +236,6 @@ class RawInputLineState extends ScrollableState<RawInputLine> { ...@@ -229,12 +236,6 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
} }
} }
@override
void dispatchOnScroll() {
super.dispatchOnScroll();
_selectionHandles?.update(_keyboardClient.inputValue.selection);
}
bool get _isAttachedToKeyboard => _keyboardHandle != null && _keyboardHandle.attached; bool get _isAttachedToKeyboard => _keyboardHandle != null && _keyboardHandle.attached;
double _contentWidth = 0.0; double _contentWidth = 0.0;
...@@ -287,8 +288,8 @@ class RawInputLineState extends ScrollableState<RawInputLine> { ...@@ -287,8 +288,8 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
if (config.onChanged != null) if (config.onChanged != null)
config.onChanged(_keyboardClient.inputValue); config.onChanged(_keyboardClient.inputValue);
if (_keyboardClient.inputValue.text != config.value.text) { if (_keyboardClient.inputValue.text != config.value.text) {
_selectionHandles?.hide(); _selectionOverlay?.hide();
_selectionHandles = null; _selectionOverlay = null;
} }
} }
...@@ -307,25 +308,27 @@ class RawInputLineState extends ScrollableState<RawInputLine> { ...@@ -307,25 +308,27 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
if (config.onChanged != null) if (config.onChanged != null)
config.onChanged(newInput); config.onChanged(newInput);
if (_selectionHandles != null) { if (_selectionOverlay != null) {
_selectionHandles.hide(); _selectionOverlay.hide();
_selectionHandles = null; _selectionOverlay = null;
} }
if (_keyboardClient.inputValue.text.isNotEmpty && if (newInput.text.isNotEmpty && config.selectionHandleBuilder != null) {
config.selectionHandleBuilder != null) { _selectionOverlay = new TextSelectionOverlay(
_selectionHandles = new TextSelectionHandles( input: newInput,
selection: selection, context: context,
debugRequiredFor: config,
renderObject: renderObject, renderObject: renderObject,
onSelectionHandleChanged: _handleSelectionHandleChanged, onSelectionOverlayChanged: _handleSelectionOverlayChanged,
builder: config.selectionHandleBuilder handleBuilder: config.selectionHandleBuilder,
toolbarBuilder: config.selectionToolbarBuilder
); );
_selectionHandles.show(context, debugRequiredFor: config); _selectionOverlay.show();
} }
} }
void _handleSelectionHandleChanged(TextSelection selection) { void _handleSelectionOverlayChanged(InputValue newInput) {
InputValue newInput = new InputValue(text: _keyboardClient.inputValue.text, selection: selection); assert(!newInput.composing.isValid); // composing range must be empty while selecting
if (config.onChanged != null) if (config.onChanged != null)
config.onChanged(newInput); config.onChanged(newInput);
} }
...@@ -357,7 +360,7 @@ class RawInputLineState extends ScrollableState<RawInputLine> { ...@@ -357,7 +360,7 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
if (_cursorTimer != null) if (_cursorTimer != null)
_stopCursorTimer(); _stopCursorTimer();
scheduleMicrotask(() { // can't hide while disposing, since it triggers a rebuild scheduleMicrotask(() { // can't hide while disposing, since it triggers a rebuild
_selectionHandles?.hide(); _selectionOverlay?.hide();
}); });
super.dispose(); super.dispose();
} }
...@@ -382,13 +385,13 @@ class RawInputLineState extends ScrollableState<RawInputLine> { ...@@ -382,13 +385,13 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
else if (_cursorTimer != null && (!focused || !config.value.selection.isCollapsed)) else if (_cursorTimer != null && (!focused || !config.value.selection.isCollapsed))
_stopCursorTimer(); _stopCursorTimer();
if (_selectionHandles != null) { if (_selectionOverlay != null) {
scheduleMicrotask(() { // can't update while disposing, since it triggers a rebuild scheduleMicrotask(() { // can't update while disposing, since it triggers a rebuild
if (focused) { if (focused) {
_selectionHandles.update(config.value.selection); _selectionOverlay.update(config.value);
} else { } else {
_selectionHandles.hide(); _selectionOverlay.hide();
_selectionHandles = null; _selectionOverlay = null;
} }
}); });
} }
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'basic.dart'; import 'basic.dart';
import 'editable.dart';
import 'framework.dart'; import 'framework.dart';
import 'gesture_detector.dart'; import 'gesture_detector.dart';
import 'overlay.dart'; import 'overlay.dart';
...@@ -24,73 +25,154 @@ enum TextSelectionHandleType { left, right, collapsed } ...@@ -24,73 +25,154 @@ enum TextSelectionHandleType { left, right, collapsed }
/// Builds a handle of the given type. /// Builds a handle of the given type.
typedef Widget TextSelectionHandleBuilder(BuildContext context, TextSelectionHandleType type); typedef Widget TextSelectionHandleBuilder(BuildContext context, TextSelectionHandleType type);
// Builds a copy/paste toolbar.
// 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 }
/// An interface for manipulating the selection, to be used by the implementor
/// of the toolbar widget.
abstract class TextSelectionDelegate {
/// Gets the current text input.
InputValue get inputValue;
/// Sets the current text input (replaces the whole line).
void set inputValue(InputValue value);
/// The copy/paste buffer. Application-wide.
String get pasteBuffer;
/// Sets the copy/paste buffer.
void set pasteBuffer(String value);
/// Hides the text selection toolbar.
void hideToolbar();
}
// TODO(mpcomplete): need to interact with the system clipboard.
String _pasteBuffer;
/// Manages a pair of text selection handles to be shown in an Overlay /// Manages a pair of text selection handles to be shown in an Overlay
/// containing the owning widget. /// containing the owning widget.
class TextSelectionHandles { class TextSelectionOverlay implements TextSelectionDelegate {
TextSelectionHandles({ TextSelectionOverlay({
TextSelection selection, InputValue input,
this.context,
this.debugRequiredFor,
this.renderObject, this.renderObject,
this.onSelectionHandleChanged, this.onSelectionOverlayChanged,
this.builder this.handleBuilder,
}): _selection = selection { this.toolbarBuilder
assert(builder != null); }): _input = input;
}
final BuildContext context;
final Widget debugRequiredFor;
// TODO(mpcomplete): what if the renderObject is removed or replaced, or // TODO(mpcomplete): what if the renderObject is removed or replaced, or
// moves? Not sure what cases I need to handle, or how to handle them. // moves? Not sure what cases I need to handle, or how to handle them.
final RenderEditableLine renderObject; final RenderEditableLine renderObject;
final ValueChanged<TextSelection> onSelectionHandleChanged; final ValueChanged<InputValue> onSelectionOverlayChanged;
final TextSelectionHandleBuilder builder; final TextSelectionHandleBuilder handleBuilder;
TextSelection _selection; final TextSelectionToolbarBuilder toolbarBuilder;
InputValue _input;
/// A pair of handles. If this is non-null, there are always 2, though the /// A pair of handles. If this is non-null, there are always 2, though the
/// second is hidden when the selection is collapsed. /// second is hidden when the selection is collapsed.
List<OverlayEntry> _handles; List<OverlayEntry> _handles;
OverlayEntry _toolbar;
TextSelection get _selection => _input.selection;
/// Shows the handles by inserting them into the [context]'s overlay. /// Shows the handles by inserting them into the [context]'s overlay.
void show(BuildContext context, { Widget debugRequiredFor }) { void show() {
assert(_handles == null); assert(_handles == null);
_handles = <OverlayEntry>[ _handles = <OverlayEntry>[
new OverlayEntry(builder: (BuildContext c) => _buildOverlay(c, _TextSelectionHandlePosition.start)), new OverlayEntry(builder: (BuildContext c) => _buildOverlay(c, _TextSelectionHandlePosition.start)),
new OverlayEntry(builder: (BuildContext c) => _buildOverlay(c, _TextSelectionHandlePosition.end)), new OverlayEntry(builder: (BuildContext c) => _buildOverlay(c, _TextSelectionHandlePosition.end)),
]; ];
_toolbar = new OverlayEntry(builder: _buildToolbar);
Overlay.of(context, debugRequiredFor: debugRequiredFor).insertAll(_handles); Overlay.of(context, debugRequiredFor: debugRequiredFor).insertAll(_handles);
Overlay.of(context, debugRequiredFor: debugRequiredFor).insert(_toolbar);
} }
/// Updates the handles after the [selection] has changed. /// Updates the handles after the [selection] has changed.
void update(TextSelection newSelection) { void update(InputValue newInput) {
_selection = newSelection; _input = newInput;
if (_handles == null)
return;
_handles[0].markNeedsBuild(); _handles[0].markNeedsBuild();
_handles[1].markNeedsBuild(); _handles[1].markNeedsBuild();
_toolbar.markNeedsBuild();
} }
/// Hides the handles. /// Hides the handles.
void hide() { void hide() {
if (_handles == null)
return;
_handles[0].remove(); _handles[0].remove();
_handles[1].remove(); _handles[1].remove();
_handles = null; _handles = null;
_toolbar.remove();
_toolbar = null;
} }
Widget _buildOverlay(BuildContext context, _TextSelectionHandlePosition position) { Widget _buildOverlay(BuildContext context, _TextSelectionHandlePosition position) {
if (_selection.isCollapsed && position == _TextSelectionHandlePosition.end) if ((_selection.isCollapsed && position == _TextSelectionHandlePosition.end) ||
handleBuilder == null)
return new Container(); // hide the second handle when collapsed return new Container(); // hide the second handle when collapsed
return new _TextSelectionHandleOverlay( return new _TextSelectionHandleOverlay(
onSelectionHandleChanged: _handleSelectionHandleChanged, onSelectionHandleChanged: _handleSelectionHandleChanged,
renderObject: renderObject, renderObject: renderObject,
selection: _selection, selection: _selection,
builder: builder, builder: handleBuilder,
position: position position: position
); );
} }
Widget _buildToolbar(BuildContext context) {
if (toolbarBuilder == null)
return new Container();
// Find the horizontal midpoint, just above the selected text.
List<TextSelectionPoint> endpoints = renderObject.getEndpointsForSelection(_selection);
Point midpoint = new Point(
(endpoints.length == 1) ?
endpoints[0].point.x :
(endpoints[0].point.x + endpoints[1].point.x) / 2.0,
endpoints[0].point.y - renderObject.size.height
);
return toolbarBuilder(context, midpoint, this);
}
void _handleSelectionHandleChanged(TextSelection newSelection) { void _handleSelectionHandleChanged(TextSelection newSelection) {
if (onSelectionHandleChanged != null) inputValue = _input.copyWith(selection: newSelection, composing: TextRange.empty);
onSelectionHandleChanged(newSelection); }
update(newSelection);
@override
InputValue get inputValue => _input;
@override
void set inputValue(InputValue value) {
update(value);
if (onSelectionOverlayChanged != null)
onSelectionOverlayChanged(value);
}
@override
String get pasteBuffer => _pasteBuffer;
@override
void set pasteBuffer(String value) {
_pasteBuffer = value;
}
@override
void hideToolbar() {
hide();
} }
} }
......
...@@ -296,4 +296,58 @@ void main() { ...@@ -296,4 +296,58 @@ void main() {
expect(inputValue.selection.extentOffset, selection.extentOffset+2); expect(inputValue.selection.extentOffset, selection.extentOffset+2);
}); });
testWidgets('Can use selection toolbar', (WidgetTester tester) {
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,
onChanged: (InputValue value) { inputValue = value; }
)
)
);
}
)
]
);
}
tester.pumpWidget(builder());
String testValue = 'abc def ghi';
enterText(testValue);
tester.pumpWidget(builder());
// Tap the text to bring up the "paste / select all" menu.
tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
tester.pumpWidget(builder());
// SELECT ALL should select all the text.
tester.tap(find.text('SELECT ALL'));
tester.pumpWidget(builder());
expect(inputValue.selection.baseOffset, 0);
expect(inputValue.selection.extentOffset, testValue.length);
// COPY should reset the selection.
tester.tap(find.text('COPY'));
tester.pumpWidget(builder());
expect(inputValue.selection.isCollapsed, true);
// Tap again to bring back the menu.
tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
tester.pumpWidget(builder());
// PASTE right before the 'e'.
tester.tap(find.text('PASTE'));
tester.pumpWidget(builder());
expect(inputValue.text, 'abc d${testValue}ef ghi');
});
} }
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