Commit 11f236ec authored by Matt Perry's avatar Matt Perry

Add basic text selection to editable text. (#3223)

Only partially works. Editing the selected text doesn't work very well,
which probably will require engine changes. Currently only draws the
selected text and allows you to manipulate the selection with draggable
selection handles.
parent 248960a7
......@@ -13,6 +13,8 @@ import 'theme.dart';
export 'package:sky_services/editing/editing.mojom.dart' show KeyboardType;
const double _kTextSelectionHandleSize = 20.0; // pixels
/// A material design text input field.
///
/// Requires one of its ancestors to be a [Material] widget.
......@@ -191,6 +193,7 @@ class _InputState extends State<Input> {
hideText: config.hideText,
cursorColor: themeData.textSelectionColor,
selectionColor: themeData.textSelectionColor,
selectionHandleBuilder: _textSelectionHandleBuilder,
keyboardType: config.keyboardType,
onChanged: onChanged,
onSubmitted: onSubmitted
......@@ -237,6 +240,32 @@ 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(
type: type,
color: Theme.of(context).textSelectionHandleColor
)
)
);
switch (type) {
case TextSelectionHandleType.left:
// Shift the child left by 100% of its width, so its top-right corner
// touches the selection endpoint.
return new FractionalTranslation(
translation: const FractionalOffset(-1.0, 0.0),
child: handle
);
case TextSelectionHandleType.right:
return handle;
}
}
}
class _FormFieldData {
......@@ -271,3 +300,40 @@ class _FormFieldData {
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.type, this.color});
final TextSelectionHandleType type;
final Color color;
@override
void paint(Canvas canvas, Size size) {
Paint paint = new Paint()..color = color;
// Each handle is a circle, with a rectangle in the top quadrant of that
// circle in the direction it's pointing. [rect] here is the size of the
// corner rect, e.g. half the diameter of the circle.
double radius = size.width/2.0;
canvas.drawCircle(new Point(radius, radius), radius, paint);
Rect rect;
switch (type) {
case TextSelectionHandleType.left:
rect = new Rect.fromLTWH(radius, 0.0, radius, radius);
break;
case TextSelectionHandleType.right:
rect = new Rect.fromLTWH(0.0, 0.0, radius, radius);
break;
}
canvas.drawRect(rect, paint);
}
@override
bool shouldRepaint(_TextSelectionHandlePainter oldPainter) {
return type != oldPainter.type ||
color != oldPainter.color;
}
}
......@@ -84,6 +84,7 @@ class ThemeData {
Color disabledColor,
Color buttonColor,
Color textSelectionColor,
Color textSelectionHandleColor,
Color backgroundColor,
Color indicatorColor,
Color hintColor,
......@@ -109,6 +110,7 @@ class ThemeData {
disabledColor ??= isDark ? Colors.white30 : Colors.black26;
buttonColor ??= isDark ? primarySwatch[600] : Colors.grey[300];
textSelectionColor ??= isDark ? accentColor : primarySwatch[200];
textSelectionHandleColor ??= isDark ? Colors.tealAccent[400] : primarySwatch[300];
backgroundColor ??= isDark ? Colors.grey[700] : primarySwatch[200];
indicatorColor ??= accentColor == primaryColor ? Colors.white : accentColor;
hintColor ??= isDark ? const Color(0x42FFFFFF) : const Color(0x4C000000);
......@@ -132,6 +134,7 @@ class ThemeData {
disabledColor: disabledColor,
buttonColor: buttonColor,
textSelectionColor: textSelectionColor,
textSelectionHandleColor: textSelectionHandleColor,
backgroundColor: backgroundColor,
indicatorColor: indicatorColor,
hintColor: hintColor,
......@@ -164,6 +167,7 @@ class ThemeData {
this.disabledColor,
this.buttonColor,
this.textSelectionColor,
this.textSelectionHandleColor,
this.backgroundColor,
this.indicatorColor,
this.hintColor,
......@@ -187,6 +191,7 @@ class ThemeData {
assert(disabledColor != null);
assert(buttonColor != null);
assert(textSelectionColor != null);
assert(textSelectionHandleColor != null);
assert(disabledColor != null);
assert(indicatorColor != null);
assert(hintColor != null);
......@@ -271,6 +276,8 @@ class ThemeData {
/// The color of text selections in text fields, such as [Input].
final Color textSelectionColor;
final Color textSelectionHandleColor;
/// A color that contrasts with the [primaryColor], e.g. used as the
/// remaining part of a progress bar.
final Color backgroundColor;
......@@ -309,6 +316,7 @@ class ThemeData {
disabledColor: Color.lerp(begin.disabledColor, end.disabledColor, t),
buttonColor: Color.lerp(begin.buttonColor, end.buttonColor, t),
textSelectionColor: Color.lerp(begin.textSelectionColor, end.textSelectionColor, t),
textSelectionHandleColor: Color.lerp(begin.textSelectionHandleColor, end.textSelectionHandleColor, t),
backgroundColor: Color.lerp(begin.backgroundColor, end.backgroundColor, t),
accentColor: Color.lerp(begin.accentColor, end.accentColor, t),
accentColorBrightness: t < 0.5 ? begin.accentColorBrightness : end.accentColorBrightness,
......@@ -339,6 +347,7 @@ class ThemeData {
(otherData.disabledColor == disabledColor) &&
(otherData.buttonColor == buttonColor) &&
(otherData.textSelectionColor == textSelectionColor) &&
(otherData.textSelectionHandleColor == textSelectionHandleColor) &&
(otherData.backgroundColor == backgroundColor) &&
(otherData.accentColor == accentColor) &&
(otherData.accentColorBrightness == accentColorBrightness) &&
......@@ -366,10 +375,11 @@ class ThemeData {
disabledColor,
buttonColor,
textSelectionColor,
textSelectionHandleColor,
backgroundColor,
accentColor,
accentColorBrightness,
hashValues( // Too many values.
accentColorBrightness,
indicatorColor,
hintColor,
errorColor,
......
......@@ -4,7 +4,6 @@
import 'dart:ui' as ui show Paragraph, ParagraphBuilder, ParagraphStyle, TextBox;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'box.dart';
......@@ -17,6 +16,21 @@ const double _kCaretWidth = 1.0; // pixels
final String _kZeroWidthSpace = new String.fromCharCode(0x200B);
/// Called when the user changes the selection (including cursor location).
typedef void SelectionChangedHandler(TextSelection selection, RenderEditableLine renderObject);
/// Represents a global screen coordinate of the point in a selection, and the
/// text direction at that point.
class TextSelectionPoint {
TextSelectionPoint(this.point, this.direction);
/// Screen coordinates of the lower left or lower right corner of the selection.
final Point point;
/// Direction of the text at this edge of the selection.
final TextDirection direction;
}
/// A single line of editable text.
class RenderEditableLine extends RenderBox {
RenderEditableLine({
......@@ -44,9 +58,11 @@ class RenderEditableLine extends RenderBox {
..onTapDown = _handleTapDown
..onTap = _handleTap
..onTapCancel = _handleTapCancel;
_longPress = new LongPressGestureRecognizer()
..onLongPress = _handleLongPress;
}
ValueChanged<TextSelection> onSelectionChanged;
SelectionChangedHandler onSelectionChanged;
ViewportDimensionsChangeCallback onPaintOffsetUpdateNeeded;
/// The text to display
......@@ -111,6 +127,32 @@ class RenderEditableLine extends RenderBox {
markNeedsPaint();
}
List<TextSelectionPoint> getEndpointsForSelection(TextSelection selection) {
_textPainter.layout(); // TODO(mpcomplete): is this hacky?
Offset offset = _paintOffset + new Offset(0.0, -_kCaretHeightOffset);
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, _contentSize.height) + offset;
return [new TextSelectionPoint(localToGlobal(start), null)];
} else {
List<ui.TextBox> boxes = _textPainter.getBoxesForSelection(selection);
Point start = new Point(boxes.first.start, boxes.first.bottom) + offset;
Point end = new Point(boxes.last.end, boxes.last.bottom) + offset;
return [
new TextSelectionPoint(localToGlobal(start), boxes.first.direction),
new TextSelectionPoint(localToGlobal(end), boxes.last.direction),
];
}
}
TextPosition getPositionForPoint(Point global) {
global += -paintOffset;
return _textPainter.getPositionForOffset(globalToLocal(global).toOffset());
}
Size _contentSize;
ui.Paragraph _layoutTemplate;
......@@ -159,16 +201,20 @@ class RenderEditableLine extends RenderBox {
bool hitTestSelf(Point position) => true;
TapGestureRecognizer _tap;
LongPressGestureRecognizer _longPress;
@override
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
if (event is PointerDownEvent && onSelectionChanged != null)
if (event is PointerDownEvent && onSelectionChanged != null) {
_tap.addPointer(event);
_longPress.addPointer(event);
}
}
Point _lastTapDownPosition;
Point _longPressPosition;
void _handleTapDown(Point globalPosition) {
_lastTapDownPosition = globalPosition;
_lastTapDownPosition = globalPosition + -paintOffset;
}
void _handleTap() {
......@@ -177,14 +223,41 @@ class RenderEditableLine extends RenderBox {
_lastTapDownPosition = null;
if (onSelectionChanged != null) {
TextPosition position = _textPainter.getPositionForOffset(globalToLocal(global).toOffset());
onSelectionChanged(new TextSelection.fromPosition(position));
onSelectionChanged(new TextSelection.fromPosition(position), this);
}
}
void _handleTapCancel() {
// longPress arrives after tapCancel, so remember the tap position.
_longPressPosition = _lastTapDownPosition;
_lastTapDownPosition = null;
}
void _handleLongPress() {
final Point global = _longPressPosition;
_longPressPosition = null;
if (onSelectionChanged != null) {
TextPosition position = _textPainter.getPositionForOffset(globalToLocal(global).toOffset());
onSelectionChanged(_selectWordAtOffset(position), this);
}
}
TextSelection _selectWordAtOffset(TextPosition position) {
// TODO(mpcomplete): Placeholder. Need to ask the engine for this info to do
// it correctly.
String str = text.toPlainText();
int start = position.offset - 1;
while (start >= 0 && str[start] != ' ')
--start;
++start;
int end = position.offset;
while (end < str.length && str[end] != ' ')
++end;
return new TextSelection(baseOffset: start, extentOffset: end);
}
BoxConstraints _constraintsForCurrentLayout; // when null, we don't have a current layout
// TODO(abarth): This logic should live in TextPainter and be shared with RenderParagraph.
......
......@@ -4,7 +4,7 @@
import 'dart:async';
import 'package:flutter/rendering.dart' show RenderEditableLine;
import 'package:flutter/rendering.dart' show RenderEditableLine, SelectionChangedHandler;
import 'package:sky_services/editing/editing.mojom.dart' as mojom;
import 'package:flutter/services.dart';
......@@ -13,6 +13,7 @@ import 'framework.dart';
import 'focus.dart';
import 'scrollable.dart';
import 'scroll_behavior.dart';
import 'text_selection.dart';
export 'package:flutter/painting.dart' show TextSelection;
export 'package:sky_services/editing/editing.mojom.dart' show KeyboardType;
......@@ -152,6 +153,7 @@ class RawInputLine extends Scrollable {
this.style,
this.cursorColor,
this.selectionColor,
this.selectionHandleBuilder,
this.keyboardType,
this.onChanged,
this.onSubmitted
......@@ -179,6 +181,8 @@ class RawInputLine extends Scrollable {
/// The color to use when painting the selection.
final Color selectionColor;
final TextSelectionHandleBuilder selectionHandleBuilder;
/// The type of keyboard to use for editing the text.
final KeyboardType keyboardType;
......@@ -198,6 +202,7 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
_KeyboardClientImpl _keyboardClient;
KeyboardHandle _keyboardHandle;
TextSelectionHandles _selectionHandles;
@override
ScrollBehavior<double, double> createScrollBehavior() => new BoundedBehavior();
......@@ -224,6 +229,12 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
}
}
@override
void dispatchOnScroll() {
super.dispatchOnScroll();
_selectionHandles?.update(_keyboardClient.inputValue.selection);
}
bool get _isAttachedToKeyboard => _keyboardHandle != null && _keyboardHandle.attached;
double _contentWidth = 0.0;
......@@ -275,6 +286,14 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
void _handleTextUpdated() {
if (config.onChanged != null)
config.onChanged(_keyboardClient.inputValue);
if (_keyboardClient.inputValue.text != config.value.text) {
_selectionHandles?.hide();
_selectionHandles = null;
} else {
// If the text is unchanged, this was probably called for a selection
// change.
_selectionHandles?.update(_keyboardClient.inputValue.selection);
}
}
void _handleTextSubmitted() {
......@@ -283,13 +302,30 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
config.onSubmitted(_keyboardClient.inputValue);
}
void _handleSelectionChanged(TextSelection selection) {
void _handleSelectionChanged(TextSelection selection, RenderEditableLine renderObject) {
// Note that this will show the keyboard for all selection changes on the
// EditableLineWidget, not just changes triggered by user gestures.
requestKeyboard();
if (config.onChanged != null)
config.onChanged(_keyboardClient.inputValue.copyWith(selection: selection));
if (_selectionHandles == null &&
_keyboardClient.inputValue.text.isNotEmpty &&
config.selectionHandleBuilder != null) {
_selectionHandles = new TextSelectionHandles(
selection: selection,
renderObject: renderObject,
onSelectionHandleChanged: _handleSelectionHandleChanged,
builder: config.selectionHandleBuilder
);
_selectionHandles.show(context, debugRequiredFor: config);
}
}
void _handleSelectionHandleChanged(TextSelection selection) {
if (config.onChanged != null)
config.onChanged(_keyboardClient.inputValue.copyWith(selection: selection));
}
/// Whether the blinking cursor is actually visible at this precise moment
......@@ -318,6 +354,9 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
_keyboardHandle.release();
if (_cursorTimer != null)
_stopCursorTimer();
scheduleMicrotask(() { // can't hide while disposing, since it triggers a rebuild
_selectionHandles?.hide();
});
super.dispose();
}
......@@ -341,6 +380,13 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
else if (_cursorTimer != null && (!focused || !config.value.selection.isCollapsed))
_stopCursorTimer();
if (_selectionHandles != null && !focused) {
scheduleMicrotask(() { // can't hide while disposing, since it triggers a rebuild
_selectionHandles.hide();
_selectionHandles = null;
});
}
return new _EditableLineWidget(
value: config.value,
style: config.style,
......@@ -375,7 +421,7 @@ class _EditableLineWidget extends LeafRenderObjectWidget {
final bool showCursor;
final Color selectionColor;
final bool hideText;
final ValueChanged<TextSelection> onSelectionChanged;
final SelectionChangedHandler onSelectionChanged;
final Offset paintOffset;
final ViewportDimensionsChangeCallback onPaintOffsetUpdateNeeded;
......
// 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 'package:flutter/rendering.dart';
import 'basic.dart';
import 'framework.dart';
import 'gesture_detector.dart';
import 'overlay.dart';
// TODO(mpcomplete): Need one for [collapsed].
/// Which type of selection handle to be displayed. With mixed-direction text,
/// both handles may be the same type. Examples:
/// LTR text: 'the <quick brown> fox'
/// The '<' is drawn with the [left] type, the '>' with the [right]
/// RTL text: 'xof <nworb kciuq> eht'
/// Same as above.
/// mixed text: '<the nwor<b quick fox'
/// Here 'the b' is selected, but 'brown' is RTL. Both are drawn with the
/// [left] type.
enum TextSelectionHandleType { left, right }
/// Builds a handle of the given type.
typedef Widget TextSelectionHandleBuilder(BuildContext context, TextSelectionHandleType type);
/// 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 }
/// Manages a pair of text selection handles to be shown in an Overlay
/// containing the owning widget.
class TextSelectionHandles {
TextSelectionHandles({
TextSelection selection,
this.renderObject,
this.onSelectionHandleChanged,
this.builder
}): _selection = selection;
// 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.
final RenderEditableLine renderObject;
final ValueChanged<TextSelection> onSelectionHandleChanged;
final TextSelectionHandleBuilder builder;
TextSelection _selection;
/// A pair of handles. If this is non-null, there are always 2, though the
/// second is hidden when the selection is collapsed.
List<OverlayEntry> _handles;
/// Shows the handles by inserting them into the [context]'s overlay.
void show(BuildContext context, { Widget debugRequiredFor }) {
assert(_handles == null);
_handles = <OverlayEntry>[
new OverlayEntry(builder: (BuildContext c) => _buildOverlay(c, _TextSelectionHandlePosition.start)),
new OverlayEntry(builder: (BuildContext c) => _buildOverlay(c, _TextSelectionHandlePosition.end)),
];
Overlay.of(context, debugRequiredFor: debugRequiredFor).insertAll(_handles);
}
/// Updates the handles after the [selection] has changed.
void update(TextSelection newSelection) {
_selection = newSelection;
_handles[0].markNeedsBuild();
_handles[1].markNeedsBuild();
}
/// Hides the handles.
void hide() {
_handles[0].remove();
_handles[1].remove();
_handles = null;
}
Widget _buildOverlay(BuildContext context, _TextSelectionHandlePosition position) {
if (_selection.isCollapsed && position == _TextSelectionHandlePosition.end)
return new Container(); // hide the second handle when collapsed
return new _TextSelectionHandleOverlay(
onSelectionHandleChanged: _handleSelectionHandleChanged,
renderObject: renderObject,
selection: _selection,
builder: builder,
position: position
);
}
void _handleSelectionHandleChanged(TextSelection newSelection) {
if (onSelectionHandleChanged != null)
onSelectionHandleChanged(newSelection);
update(newSelection);
}
}
/// This widget represents a single draggable text selection handle.
class _TextSelectionHandleOverlay extends StatefulWidget {
_TextSelectionHandleOverlay({
Key key,
this.selection,
this.position,
this.renderObject,
this.onSelectionHandleChanged,
this.builder
}) : super(key: key);
final TextSelection selection;
final _TextSelectionHandlePosition position;
final RenderEditableLine renderObject;
final ValueChanged<TextSelection> onSelectionHandleChanged;
final TextSelectionHandleBuilder builder;
@override
_TextSelectionHandleOverlayState createState() => new _TextSelectionHandleOverlayState();
}
class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay> {
Point _dragPosition;
void _handleDragStart(Point position) {
_dragPosition = position;
}
void _handleDragUpdate(double delta) {
_dragPosition += new Offset(delta, 0.0);
TextPosition position = config.renderObject.getPositionForPoint(_dragPosition);
if (config.selection.isCollapsed) {
config.onSelectionHandleChanged(new TextSelection.fromPosition(position));
return;
}
TextSelection newSelection;
switch (config.position) {
case _TextSelectionHandlePosition.start:
newSelection = new TextSelection(
baseOffset: position.offset,
extentOffset: config.selection.extentOffset
);
break;
case _TextSelectionHandlePosition.end:
newSelection = new TextSelection(
baseOffset: config.selection.baseOffset,
extentOffset: position.offset
);
break;
}
if (newSelection.baseOffset >= newSelection.extentOffset)
return; // don't allow order swapping.
config.onSelectionHandleChanged(newSelection);
}
@override
Widget build(BuildContext context) {
List<TextSelectionPoint> endpoints = config.renderObject.getEndpointsForSelection(config.selection);
Point point;
TextSelectionHandleType type;
switch (config.position) {
case _TextSelectionHandlePosition.start:
point = endpoints[0].point;
type = _chooseType(endpoints[0], TextSelectionHandleType.left, TextSelectionHandleType.right);
break;
case _TextSelectionHandlePosition.end:
// [endpoints] will only contain 1 point for collapsed selections, in
// which case we shouldn't be building the [end] handle.
assert(endpoints.length == 2);
point = endpoints[1].point;
type = _chooseType(endpoints[1], TextSelectionHandleType.right, TextSelectionHandleType.left);
break;
}
return new GestureDetector(
onHorizontalDragStart: _handleDragStart,
onHorizontalDragUpdate: _handleDragUpdate,
child: new Stack(
children: <Widget>[
new Positioned(
left: point.x,
top: point.y,
child: config.builder(context, type)
)
]
)
);
}
TextSelectionHandleType _chooseType(
TextSelectionPoint endpoint,
TextSelectionHandleType ltrType,
TextSelectionHandleType rtlType
) {
// [direction] is null when it doesn't matter.
switch (endpoint.direction ?? TextDirection.ltr) {
case TextDirection.ltr:
return ltrType;
case TextDirection.rtl:
return rtlType;
}
}
}
......@@ -45,6 +45,7 @@ export 'src/widgets/scrollable.dart';
export 'src/widgets/semantics_debugger.dart';
export 'src/widgets/status_transitions.dart';
export 'src/widgets/table.dart';
export 'src/widgets/text_selection.dart';
export 'src/widgets/title.dart';
export 'src/widgets/transitions.dart';
export 'src/widgets/unique_widget.dart';
......
......@@ -31,6 +31,15 @@ void main() {
MockKeyboard mockKeyboard = new MockKeyboard();
serviceMocker.registerMockService(mojom.Keyboard.serviceName, mockKeyboard);
void enterText(String testValue) {
// Simulate entry of text through the keyboard.
expect(mockKeyboard.client, isNotNull);
mockKeyboard.client.updateEditingState(new mojom.EditingState()
..text = testValue
..composingBase = 0
..composingExtent = testValue.length);
}
test('Editable text has consistent size', () {
testWidgets((WidgetTester tester) {
GlobalKey inputKey = new GlobalKey();
......@@ -56,13 +65,8 @@ void main() {
RenderBox inputBox = findInputBox();
Size emptyInputSize = inputBox.size;
void enterText(String testValue) {
// Simulate entry of text through the keyboard.
expect(mockKeyboard.client, isNotNull);
mockKeyboard.client.updateEditingState(new mojom.EditingState()
..text = testValue
..composingBase = 0
..composingExtent = testValue.length);
void checkText(String testValue) {
enterText(testValue);
// Check that the onChanged event handler fired.
expect(inputValue.text, equals(testValue));
......@@ -70,11 +74,11 @@ void main() {
tester.pumpWidget(builder());
}
enterText(' ');
checkText(' ');
expect(findInputBox(), equals(inputBox));
expect(inputBox.size, equals(emptyInputSize));
enterText('Test');
checkText('Test');
expect(findInputBox(), equals(inputBox));
expect(inputBox.size, equals(emptyInputSize));
});
......@@ -152,4 +156,155 @@ void main() {
tester.pump();
});
});
// Returns the first RenderEditableLine.
RenderEditableLine findRenderEditableLine(WidgetTester tester) {
RenderObject root = tester.renderObjectOf(find.byType(RawInputLine));
expect(root, isNotNull);
RenderEditableLine renderLine;
void recursiveFinder(RenderObject child) {
if (child is RenderEditableLine) {
renderLine = child;
return;
}
child.visitChildren(recursiveFinder);
}
root.visitChildren(recursiveFinder);
expect(renderLine, isNotNull);
return renderLine;
}
Point textOffsetToPosition(WidgetTester tester, int offset) {
RenderEditableLine renderLine = findRenderEditableLine(tester);
List<TextSelectionPoint> endpoints = renderLine.getEndpointsForSelection(
new TextSelection.collapsed(offset: offset));
expect(endpoints.length, 1);
return endpoints[0].point;
}
test('Can long press to select', () {
testWidgets((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);
expect(inputValue.text, testValue);
tester.pumpWidget(builder());
expect(inputValue.selection.isCollapsed, true);
// Long press the 'e' to select 'def'.
Point ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
TestGesture gesture = tester.startGesture(ePos, pointer: 7);
tester.pump(const Duration(seconds: 2));
gesture.up();
tester.pump();
// 'def' is selected.
expect(inputValue.selection.baseOffset, testValue.indexOf('d'));
expect(inputValue.selection.extentOffset, testValue.indexOf('f')+1);
});
});
test('Can drag handles to change selection', () {
testWidgets((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());
// Long press the 'e' to select 'def'.
Point ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
TestGesture gesture = tester.startGesture(ePos, pointer: 7);
tester.pump(const Duration(seconds: 2));
gesture.up();
tester.pump();
TextSelection selection = inputValue.selection;
RenderEditableLine renderLine = findRenderEditableLine(tester);
List<TextSelectionPoint> endpoints = renderLine.getEndpointsForSelection(
selection);
expect(endpoints.length, 2);
// Drag the right handle 2 letters to the right.
// Note: use a small offset because the endpoint is on the very corner
// of the handle.
Point handlePos = endpoints[1].point + new Offset(1.0, 1.0);
Point newHandlePos = textOffsetToPosition(tester, selection.extentOffset+2);
gesture = tester.startGesture(handlePos, pointer: 7);
tester.pump();
gesture.moveTo(newHandlePos);
tester.pump();
gesture.up();
tester.pump();
expect(inputValue.selection.baseOffset, selection.baseOffset);
expect(inputValue.selection.extentOffset, selection.extentOffset+2);
// Drag the left handle 2 letters to the left.
handlePos = endpoints[0].point + new Offset(-1.0, 1.0);
newHandlePos = textOffsetToPosition(tester, selection.baseOffset-2);
gesture = tester.startGesture(handlePos, pointer: 7);
tester.pump();
gesture.moveTo(newHandlePos);
tester.pump();
gesture.up();
tester.pumpWidget(builder());
expect(inputValue.selection.baseOffset, selection.baseOffset-2);
expect(inputValue.selection.extentOffset, selection.extentOffset+2);
});
});
}
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