Unverified Commit ad2e3eb3 authored by jslavitz's avatar jslavitz Committed by GitHub

Adds support for floating cursor (#25384)

* Adds support for floating cursor.
parent b4f1d5a9
...@@ -672,6 +672,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK ...@@ -672,6 +672,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
cursorWidth: widget.cursorWidth, cursorWidth: widget.cursorWidth,
cursorRadius: widget.cursorRadius, cursorRadius: widget.cursorRadius,
cursorColor: widget.cursorColor, cursorColor: widget.cursorColor,
backgroundCursorColor: CupertinoColors.inactiveGray,
scrollPadding: widget.scrollPadding, scrollPadding: widget.scrollPadding,
keyboardAppearance: keyboardAppearance, keyboardAppearance: keyboardAppearance,
), ),
......
...@@ -622,6 +622,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi ...@@ -622,6 +622,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
cursorWidth: widget.cursorWidth, cursorWidth: widget.cursorWidth,
cursorRadius: widget.cursorRadius, cursorRadius: widget.cursorRadius,
cursorColor: widget.cursorColor ?? Theme.of(context).cursorColor, cursorColor: widget.cursorColor ?? Theme.of(context).cursorColor,
backgroundCursorColor: CupertinoColors.inactiveGray,
scrollPadding: widget.scrollPadding, scrollPadding: widget.scrollPadding,
keyboardAppearance: keyboardAppearance, keyboardAppearance: keyboardAppearance,
enableInteractiveSelection: widget.enableInteractiveSelection, enableInteractiveSelection: widget.enableInteractiveSelection,
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
//ignore: Remove this once Google catches up with dev.4 Dart. //ignore: Remove this once Google catches up with dev.4 Dart.
import 'dart:async'; import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui' as ui show TextBox; import 'dart:ui' as ui show TextBox, lerpDouble;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
...@@ -19,6 +19,13 @@ import 'viewport_offset.dart'; ...@@ -19,6 +19,13 @@ import 'viewport_offset.dart';
const double _kCaretGap = 1.0; // pixels const double _kCaretGap = 1.0; // pixels
const double _kCaretHeightOffset = 2.0; // pixels const double _kCaretHeightOffset = 2.0; // pixels
// The additional size on the x and y axis with which to expand the prototype
// cursor to render the floating cursor in pixels.
const Offset _kFloatingCaretSizeIncrease = Offset(0.5, 2.0);
// The corner radius of the floating cursor in pixels.
const double _kFloatingCaretRadius = 1.0;
/// Signature for the callback that reports when the user changes the selection /// Signature for the callback that reports when the user changes the selection
/// (including the cursor location). /// (including the cursor location).
/// ///
...@@ -129,6 +136,7 @@ class RenderEditable extends RenderBox { ...@@ -129,6 +136,7 @@ class RenderEditable extends RenderBox {
@required TextDirection textDirection, @required TextDirection textDirection,
TextAlign textAlign = TextAlign.start, TextAlign textAlign = TextAlign.start,
Color cursorColor, Color cursorColor,
Color backgroundCursorColor,
ValueNotifier<bool> showCursor, ValueNotifier<bool> showCursor,
bool hasFocus, bool hasFocus,
int maxLines = 1, int maxLines = 1,
...@@ -143,6 +151,7 @@ class RenderEditable extends RenderBox { ...@@ -143,6 +151,7 @@ class RenderEditable extends RenderBox {
Locale locale, Locale locale,
double cursorWidth = 1.0, double cursorWidth = 1.0,
Radius cursorRadius, Radius cursorRadius,
EdgeInsets floatingCursorAddedMargin = const EdgeInsets.fromLTRB(3, 6, 3, 6),
bool enableInteractiveSelection = true, bool enableInteractiveSelection = true,
@required this.textSelectionDelegate, @required this.textSelectionDelegate,
}) : assert(textAlign != null), }) : assert(textAlign != null),
...@@ -162,6 +171,7 @@ class RenderEditable extends RenderBox { ...@@ -162,6 +171,7 @@ class RenderEditable extends RenderBox {
locale: locale, locale: locale,
), ),
_cursorColor = cursorColor, _cursorColor = cursorColor,
_backgroundCursorColor = backgroundCursorColor,
_showCursor = showCursor ?? ValueNotifier<bool>(false), _showCursor = showCursor ?? ValueNotifier<bool>(false),
_hasFocus = hasFocus ?? false, _hasFocus = hasFocus ?? false,
_maxLines = maxLines, _maxLines = maxLines,
...@@ -170,6 +180,7 @@ class RenderEditable extends RenderBox { ...@@ -170,6 +180,7 @@ class RenderEditable extends RenderBox {
_offset = offset, _offset = offset,
_cursorWidth = cursorWidth, _cursorWidth = cursorWidth,
_cursorRadius = cursorRadius, _cursorRadius = cursorRadius,
_floatingCursorAddedMargin = floatingCursorAddedMargin,
_enableInteractiveSelection = enableInteractiveSelection, _enableInteractiveSelection = enableInteractiveSelection,
_obscureText = obscureText { _obscureText = obscureText {
assert(_showCursor != null); assert(_showCursor != null);
...@@ -566,6 +577,19 @@ class RenderEditable extends RenderBox { ...@@ -566,6 +577,19 @@ class RenderEditable extends RenderBox {
markNeedsPaint(); markNeedsPaint();
} }
/// The color to use when painting the cursor aligned to the text while
/// rendering the floating cursor.
///
/// The default is light grey.
Color get backgroundCursorColor => _backgroundCursorColor;
Color _backgroundCursorColor;
set backgroundCursorColor(Color value) {
if (backgroundCursorColor == value)
return;
_backgroundCursorColor = value;
markNeedsPaint();
}
/// Whether to paint the cursor. /// Whether to paint the cursor.
ValueNotifier<bool> get showCursor => _showCursor; ValueNotifier<bool> get showCursor => _showCursor;
ValueNotifier<bool> _showCursor; ValueNotifier<bool> _showCursor;
...@@ -701,6 +725,23 @@ class RenderEditable extends RenderBox { ...@@ -701,6 +725,23 @@ class RenderEditable extends RenderBox {
markNeedsPaint(); markNeedsPaint();
} }
/// The padding applied to text field. Used to determine the bounds when
/// moving the floating cursor.
///
/// Defaults to a padding with left, right set to 3 and top, bottom to 6.
EdgeInsets get floatingCursorAddedMargin => _floatingCursorAddedMargin;
EdgeInsets _floatingCursorAddedMargin;
set floatingCursorAddedMargin(EdgeInsets value) {
if (_floatingCursorAddedMargin == value)
return;
_floatingCursorAddedMargin = value;
markNeedsPaint();
}
bool _floatingCursorOn = false;
Offset _floatingCursorOffset;
TextPosition _floatingCursorTextPosition;
/// If false, [describeSemanticsConfiguration] will not set the /// If false, [describeSemanticsConfiguration] will not set the
/// configuration's cursor motion or set selection callbacks. /// configuration's cursor motion or set selection callbacks.
/// ///
...@@ -1205,11 +1246,13 @@ class RenderEditable extends RenderBox { ...@@ -1205,11 +1246,13 @@ class RenderEditable extends RenderBox {
offset.applyContentDimensions(0.0, _maxScrollExtent); offset.applyContentDimensions(0.0, _maxScrollExtent);
} }
void _paintCaret(Canvas canvas, Offset effectiveOffset) { void _paintCaret(Canvas canvas, Offset effectiveOffset, TextPosition textPosition) {
assert(_textLayoutLastWidth == constraints.maxWidth); assert(_textLayoutLastWidth == constraints.maxWidth);
final Offset caretOffset = _textPainter.getOffsetForCaret(_selection.extent, _caretPrototype); final Offset caretOffset = _textPainter.getOffsetForCaret(textPosition, _caretPrototype);
// If the floating cursor is enabled, the text cursor's color is [backgroundCursorColor] while
// the floating cursor's color is _cursorColor;
final Paint paint = Paint() final Paint paint = Paint()
..color = _cursorColor; ..color = _floatingCursorOn ? backgroundCursorColor : _cursorColor;
final Rect caretRect = _caretPrototype.shift(caretOffset + effectiveOffset); final Rect caretRect = _caretPrototype.shift(caretOffset + effectiveOffset);
...@@ -1227,6 +1270,112 @@ class RenderEditable extends RenderBox { ...@@ -1227,6 +1270,112 @@ class RenderEditable extends RenderBox {
} }
} }
/// Sets the screen position of the floating cursor and the text position
/// closest to the cursor.
void setFloatingCursor(FloatingCursorDragState state, Offset boundedOffset, TextPosition lastTextPosition, { double resetLerpValue }) {
assert(state != null);
assert(boundedOffset != null);
assert(lastTextPosition != null);
if (state == FloatingCursorDragState.Start) {
_relativeOrigin = const Offset(0, 0);
_previousOffset = null;
_resetOriginOnBottom = false;
_resetOriginOnTop = false;
_resetOriginOnRight = false;
_resetOriginOnBottom = false;
}
_floatingCursorOn = state != FloatingCursorDragState.End;
_resetFloatingCursorAnimationValue = resetLerpValue;
if(_floatingCursorOn) {
_floatingCursorOffset = boundedOffset;
_floatingCursorTextPosition = lastTextPosition;
}
markNeedsPaint();
}
void _paintFloatingCaret(Canvas canvas, Offset effectiveOffset) {
assert(_textLayoutLastWidth == constraints.maxWidth);
assert(_floatingCursorOn);
final Paint paint = Paint()..color = _cursorColor;
double sizeAdjustmentX = _kFloatingCaretSizeIncrease.dx;
double sizeAdjustmentY = _kFloatingCaretSizeIncrease.dy;
if(_resetFloatingCursorAnimationValue != null) {
sizeAdjustmentX = ui.lerpDouble(sizeAdjustmentX, 0, _resetFloatingCursorAnimationValue);
sizeAdjustmentY = ui.lerpDouble(sizeAdjustmentY, 0, _resetFloatingCursorAnimationValue);
}
final Rect floatingCaretPrototype = Rect.fromLTRB(_caretPrototype.left - sizeAdjustmentX,
_caretPrototype.top - sizeAdjustmentY,
_caretPrototype.right + sizeAdjustmentX,
_caretPrototype.bottom + sizeAdjustmentY);
final Rect caretRect = floatingCaretPrototype.shift(effectiveOffset);
const Radius floatingCursorRadius = Radius.circular(_kFloatingCaretRadius);
final RRect caretRRect = RRect.fromRectAndRadius(caretRect, floatingCursorRadius);
canvas.drawRRect(caretRRect, paint);
}
// The relative origin in relation to the distance the user has theoretically
// dragged the floating cursor offscreen. This value is used to account for the
// difference in the rendering position and the raw offset value.
Offset _relativeOrigin = const Offset(0, 0);
Offset _previousOffset;
bool _resetOriginOnLeft = false;
bool _resetOriginOnRight = false;
bool _resetOriginOnTop = false;
bool _resetOriginOnBottom = false;
double _resetFloatingCursorAnimationValue;
/// Returns the position within the text field closest to the raw cursor offset.
Offset calculateBoundedFloatingCursorOffset(Offset rawCursorOffset) {
Offset deltaPosition = const Offset(0, 0);
final double topBound = -floatingCursorAddedMargin.top;
final double bottomBound = _textPainter.height - preferredLineHeight + floatingCursorAddedMargin.bottom;
final double leftBound = -floatingCursorAddedMargin.left;
final double rightBound = _textPainter.width + floatingCursorAddedMargin.right;
if (_previousOffset != null)
deltaPosition = rawCursorOffset - _previousOffset;
// If the raw cursor offset has gone off an edge, we want to reset the relative
// origin of the dragging when the user drags back into the field.
if (_resetOriginOnLeft && deltaPosition.dx > 0) {
_relativeOrigin = Offset(rawCursorOffset.dx - leftBound, _relativeOrigin.dy);
_resetOriginOnLeft = false;
} else if (_resetOriginOnRight && deltaPosition.dx < 0) {
_relativeOrigin = Offset(rawCursorOffset.dx - rightBound, _relativeOrigin.dy);
_resetOriginOnRight = false;
}
if (_resetOriginOnTop && deltaPosition.dy > 0) {
_relativeOrigin = Offset(_relativeOrigin.dx, rawCursorOffset.dy - topBound);
_resetOriginOnTop = false;
} else if (_resetOriginOnBottom && deltaPosition.dy < 0) {
_relativeOrigin = Offset(_relativeOrigin.dx, rawCursorOffset.dy - bottomBound);
_resetOriginOnBottom = false;
}
final double currentX = rawCursorOffset.dx - _relativeOrigin.dx;
final double currentY = rawCursorOffset.dy - _relativeOrigin.dy;
final double adjustedX = math.min(math.max(currentX, leftBound), rightBound);
final double adjustedY = math.min(math.max(currentY, topBound), bottomBound);
final Offset adjustedOffset = Offset(adjustedX, adjustedY);
if (currentX < leftBound && deltaPosition.dx < 0) {
_resetOriginOnLeft = true;
} else if(currentX > rightBound && deltaPosition.dx > 0)
_resetOriginOnRight = true;
if (currentY < topBound && deltaPosition.dy < 0)
_resetOriginOnTop = true;
else if (currentY > bottomBound && deltaPosition.dy > 0)
_resetOriginOnBottom = true;
_previousOffset = rawCursorOffset;
return adjustedOffset;
}
void _paintSelection(Canvas canvas, Offset effectiveOffset) { void _paintSelection(Canvas canvas, Offset effectiveOffset) {
assert(_textLayoutLastWidth == constraints.maxWidth); assert(_textLayoutLastWidth == constraints.maxWidth);
assert(_selectionRects != null); assert(_selectionRects != null);
...@@ -1238,17 +1387,20 @@ class RenderEditable extends RenderBox { ...@@ -1238,17 +1387,20 @@ class RenderEditable extends RenderBox {
void _paintContents(PaintingContext context, Offset offset) { void _paintContents(PaintingContext context, Offset offset) {
assert(_textLayoutLastWidth == constraints.maxWidth); assert(_textLayoutLastWidth == constraints.maxWidth);
final Offset effectiveOffset = offset + _paintOffset; final Offset effectiveOffset = offset + _paintOffset;
if (_selection != null && !_floatingCursorOn) {
if (_selection != null) {
if (_selection.isCollapsed && _showCursor.value && cursorColor != null) { if (_selection.isCollapsed && _showCursor.value && cursorColor != null) {
_paintCaret(context.canvas, effectiveOffset); _paintCaret(context.canvas, effectiveOffset, _selection.extent);
} else if (!_selection.isCollapsed && _selectionColor != null) { } else if (!_selection.isCollapsed && _selectionColor != null) {
_selectionRects ??= _textPainter.getBoxesForSelection(_selection); _selectionRects ??= _textPainter.getBoxesForSelection(_selection);
_paintSelection(context.canvas, effectiveOffset); _paintSelection(context.canvas, effectiveOffset);
} }
} }
_textPainter.paint(context.canvas, effectiveOffset); _textPainter.paint(context.canvas, effectiveOffset);
if (_floatingCursorOn) {
if (_resetFloatingCursorAnimationValue == null)
_paintCaret(context.canvas, effectiveOffset, _floatingCursorTextPosition);
_paintFloatingCaret(context.canvas, _floatingCursorOffset);
}
} }
@override @override
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io' show Platform; import 'dart:io' show Platform;
import 'dart:ui' show TextAffinity, hashValues; import 'dart:ui' show TextAffinity, hashValues, Offset;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
...@@ -439,6 +439,40 @@ TextAffinity _toTextAffinity(String affinity) { ...@@ -439,6 +439,40 @@ TextAffinity _toTextAffinity(String affinity) {
return null; return null;
} }
/// A floating cursor state the user has induced by force pressing an iOS
/// keyboard.
enum FloatingCursorDragState {
/// A user has just activated a floating cursor.
Start,
/// A user is dragging a floating cursor.
Update,
/// A user has lifted their finger off the screen after using a floating
/// cursor.
End,
}
/// The current state and position of the floating cursor.
class RawFloatingCursorPoint {
/// Creates information for setting the position and state of a floating
/// cursor.
///
/// [state] must not be null and [offset] must not be null if the state is
/// [FloatingCursorDragState.Update].
RawFloatingCursorPoint({
this.offset,
@required this.state,
}) : assert(state != null),
assert(state == FloatingCursorDragState.Update ? offset != null : true);
/// The raw position of the floating cursor as determined by the iOS sdk.
final Offset offset;
/// The state of the floating cursor.
final FloatingCursorDragState state;
}
/// The current text, selection, and composing state for editing a run of text. /// The current text, selection, and composing state for editing a run of text.
@immutable @immutable
class TextEditingValue { class TextEditingValue {
...@@ -566,6 +600,9 @@ abstract class TextInputClient { ...@@ -566,6 +600,9 @@ abstract class TextInputClient {
/// Requests that this client perform the given action. /// Requests that this client perform the given action.
void performAction(TextInputAction action); void performAction(TextInputAction action);
/// Updates the floating cursor position and state.
void updateFloatingCursor(RawFloatingCursorPoint point);
} }
/// An interface for interacting with a text input control. /// An interface for interacting with a text input control.
...@@ -648,6 +685,26 @@ TextInputAction _toTextInputAction(String action) { ...@@ -648,6 +685,26 @@ TextInputAction _toTextInputAction(String action) {
throw FlutterError('Unknown text input action: $action'); throw FlutterError('Unknown text input action: $action');
} }
FloatingCursorDragState _toTextCursorAction(String state) {
switch (state) {
case 'FloatingCursorDragState.start':
return FloatingCursorDragState.Start;
case 'FloatingCursorDragState.update':
return FloatingCursorDragState.Update;
case 'FloatingCursorDragState.end':
return FloatingCursorDragState.End;
}
throw FlutterError('Unknown text cursor action: $state');
}
RawFloatingCursorPoint _toTextPoint(FloatingCursorDragState state, Map<String, dynamic> encoded) {
assert(state != null, 'You must provide a state to set a new editing point.');
assert(encoded['X'] != null, 'You must provide a value for the horizontal location of the floating cursor.');
assert(encoded['Y'] != null, 'You must provide a value for the vertical location of the floating cursor.');
final Offset offset = state == FloatingCursorDragState.Update ? Offset(encoded['X'], encoded['Y']) : const Offset(0, 0);
return RawFloatingCursorPoint(offset: offset, state: state);
}
class _TextInputClientHandler { class _TextInputClientHandler {
_TextInputClientHandler() { _TextInputClientHandler() {
SystemChannels.textInput.setMethodCallHandler(_handleTextInputInvocation); SystemChannels.textInput.setMethodCallHandler(_handleTextInputInvocation);
...@@ -671,6 +728,9 @@ class _TextInputClientHandler { ...@@ -671,6 +728,9 @@ class _TextInputClientHandler {
case 'TextInputClient.performAction': case 'TextInputClient.performAction':
_currentConnection._client.performAction(_toTextInputAction(args[1])); _currentConnection._client.performAction(_toTextInputAction(args[1]));
break; break;
case 'TextInputClient.updateFloatingCursor':
_currentConnection._client.updateFloatingCursor(_toTextPoint(_toTextCursorAction(args[1]), args[2]));
break;
default: default:
throw MissingPluginException(); throw MissingPluginException();
} }
......
...@@ -21,6 +21,7 @@ import 'scroll_controller.dart'; ...@@ -21,6 +21,7 @@ import 'scroll_controller.dart';
import 'scroll_physics.dart'; import 'scroll_physics.dart';
import 'scrollable.dart'; import 'scrollable.dart';
import 'text_selection.dart'; import 'text_selection.dart';
import 'ticker_provider.dart';
export 'package:flutter/services.dart' show TextEditingValue, TextSelection, TextInputType; export 'package:flutter/services.dart' show TextEditingValue, TextSelection, TextInputType;
export 'package:flutter/rendering.dart' show SelectionChangedCause; export 'package:flutter/rendering.dart' show SelectionChangedCause;
...@@ -182,9 +183,9 @@ class EditableText extends StatefulWidget { ...@@ -182,9 +183,9 @@ class EditableText extends StatefulWidget {
/// [TextInputType.text] unless [maxLines] is greater than one, when it will /// [TextInputType.text] unless [maxLines] is greater than one, when it will
/// default to [TextInputType.multiline]. /// default to [TextInputType.multiline].
/// ///
/// The [controller], [focusNode], [style], [cursorColor], [textAlign], /// The [controller], [focusNode], [style], [cursorColor], [backgroundCursorColor],
/// [rendererIgnoresPointer], and [enableInteractiveSelection] arguments must /// [textAlign], [rendererIgnoresPointer], and [enableInteractiveSelection]
/// not be null. /// arguments must not be null.
EditableText({ EditableText({
Key key, Key key,
@required this.controller, @required this.controller,
...@@ -193,6 +194,7 @@ class EditableText extends StatefulWidget { ...@@ -193,6 +194,7 @@ class EditableText extends StatefulWidget {
this.autocorrect = true, this.autocorrect = true,
@required this.style, @required this.style,
@required this.cursorColor, @required this.cursorColor,
@required this.backgroundCursorColor,
this.textAlign = TextAlign.start, this.textAlign = TextAlign.start,
this.textDirection, this.textDirection,
this.locale, this.locale,
...@@ -221,6 +223,7 @@ class EditableText extends StatefulWidget { ...@@ -221,6 +223,7 @@ class EditableText extends StatefulWidget {
assert(autocorrect != null), assert(autocorrect != null),
assert(style != null), assert(style != null),
assert(cursorColor != null), assert(cursorColor != null),
assert(backgroundCursorColor != null),
assert(textAlign != null), assert(textAlign != null),
assert(maxLines == null || maxLines > 0), assert(maxLines == null || maxLines > 0),
assert(autofocus != null), assert(autofocus != null),
...@@ -324,6 +327,13 @@ class EditableText extends StatefulWidget { ...@@ -324,6 +327,13 @@ class EditableText extends StatefulWidget {
/// Cannot be null. /// Cannot be null.
final Color cursorColor; final Color cursorColor;
/// The color to use when painting the background cursor aligned with the text
/// while rendering the floating cursor.
///
/// Cannot be null. By default it is the disabled grey color from
/// CupertinoColors.
final Color backgroundCursorColor;
/// {@template flutter.widgets.editableText.maxLines} /// {@template flutter.widgets.editableText.maxLines}
/// The maximum number of lines for the text to span, wrapping if necessary. /// The maximum number of lines for the text to span, wrapping if necessary.
/// ///
...@@ -491,7 +501,7 @@ class EditableText extends StatefulWidget { ...@@ -491,7 +501,7 @@ class EditableText extends StatefulWidget {
} }
/// State for a [EditableText]. /// State for a [EditableText].
class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin<EditableText>, WidgetsBindingObserver implements TextInputClient, TextSelectionDelegate { class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin<EditableText>, WidgetsBindingObserver, TickerProviderStateMixin<EditableText> implements TextInputClient, TextSelectionDelegate {
Timer _cursorTimer; Timer _cursorTimer;
final ValueNotifier<bool> _showCursor = ValueNotifier<bool>(false); final ValueNotifier<bool> _showCursor = ValueNotifier<bool>(false);
final GlobalKey _editableKey = GlobalKey(); final GlobalKey _editableKey = GlobalKey();
...@@ -503,6 +513,12 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -503,6 +513,12 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
final LayerLink _layerLink = LayerLink(); final LayerLink _layerLink = LayerLink();
bool _didAutoFocus = false; bool _didAutoFocus = false;
// The time it takes for the floating cursor to snap to the text aligned
// cursor position after the user has finished placing it.
static const Duration _floatingCursorResetTime = Duration(milliseconds: 125);
AnimationController _floatingCursorResetController;
@override @override
bool get wantKeepAlive => widget.focusNode.hasFocus; bool get wantKeepAlive => widget.focusNode.hasFocus;
...@@ -514,6 +530,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -514,6 +530,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
widget.controller.addListener(_didChangeTextEditingValue); widget.controller.addListener(_didChangeTextEditingValue);
widget.focusNode.addListener(_handleFocusChanged); widget.focusNode.addListener(_handleFocusChanged);
_scrollController.addListener(() { _selectionOverlay?.updateForScroll(); }); _scrollController.addListener(() { _selectionOverlay?.updateForScroll(); });
_floatingCursorResetController = AnimationController(vsync: this);
_floatingCursorResetController.addListener(_onFloatingCursorResetTick);
} }
@override @override
...@@ -595,6 +613,72 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -595,6 +613,72 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
} }
// The original position of the caret on FloatingCursorDragState.start.
Rect _startCaretRect;
// The most recent text position as determined by the location of the floating
// cursor.
TextPosition _lastTextPosition;
// The offset of the floating cursor as determined from the first update call.
Offset _pointOffsetOrigin;
// The most recent position of the floating cursor.
Offset _lastBoundedOffset;
// Because the center of the cursor is preferredLineHeight / 2 below the touch
// origin, but the touch origin is used to determine which line the cursor is
// on, we need this offset to correctly render and move the cursor.
Offset get _floatingCursorOffset => Offset(0, renderEditable.preferredLineHeight / 2);
@override
void updateFloatingCursor(RawFloatingCursorPoint point) {
switch(point.state){
case FloatingCursorDragState.Start:
final TextPosition currentTextPosition = TextPosition(offset: renderEditable.selection.baseOffset);
_startCaretRect = renderEditable.getLocalRectForCaret(currentTextPosition);
renderEditable.setFloatingCursor(point.state, _startCaretRect.center - _floatingCursorOffset, currentTextPosition);
break;
case FloatingCursorDragState.Update:
// We want to send in points that are centered around a (0,0) origin, so we cache the
// position on the first update call.
if (_pointOffsetOrigin != null) {
final Offset centeredPoint = point.offset - _pointOffsetOrigin;
final Offset rawCursorOffset = _startCaretRect.center + centeredPoint - _floatingCursorOffset;
_lastBoundedOffset = renderEditable.calculateBoundedFloatingCursorOffset(rawCursorOffset);
_lastTextPosition = renderEditable.getPositionForPoint(renderEditable.localToGlobal(_lastBoundedOffset + _floatingCursorOffset));
renderEditable.setFloatingCursor(point.state, _lastBoundedOffset, _lastTextPosition);
} else {
_pointOffsetOrigin = point.offset;
}
break;
case FloatingCursorDragState.End:
_floatingCursorResetController.value = 0.0;
_floatingCursorResetController.animateTo(1.0, duration: _floatingCursorResetTime, curve: Curves.decelerate);
break;
}
}
void _onFloatingCursorResetTick() {
final Offset finalPosition = renderEditable.getLocalRectForCaret(_lastTextPosition).center - _floatingCursorOffset;
if (_floatingCursorResetController.isCompleted) {
renderEditable.setFloatingCursor(FloatingCursorDragState.End, finalPosition, _lastTextPosition);
if (_lastTextPosition.offset != renderEditable.selection.baseOffset)
// The cause is technically the force cursor, but the cause is listed as tap as the desired functionality is the same.
_handleSelectionChanged(TextSelection.collapsed(offset: _lastTextPosition.offset), renderEditable, SelectionChangedCause.tap);
_startCaretRect = null;
_lastTextPosition = null;
_pointOffsetOrigin = null;
_lastBoundedOffset = null;
} else {
final double lerpValue = _floatingCursorResetController.value;
final double lerpX = ui.lerpDouble(_lastBoundedOffset.dx, finalPosition.dx, lerpValue);
final double lerpY = ui.lerpDouble(_lastBoundedOffset.dy, finalPosition.dy, lerpValue);
renderEditable.setFloatingCursor(FloatingCursorDragState.Update, Offset(lerpX, lerpY), _lastTextPosition, resetLerpValue: lerpValue);
}
}
void _finalizeEditing(bool shouldUnfocus) { void _finalizeEditing(bool shouldUnfocus) {
// Take any actions necessary now that the user has completed editing. // Take any actions necessary now that the user has completed editing.
if (widget.onEditingComplete != null) { if (widget.onEditingComplete != null) {
...@@ -969,6 +1053,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -969,6 +1053,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
textSpan: buildTextSpan(), textSpan: buildTextSpan(),
value: _value, value: _value,
cursorColor: widget.cursorColor, cursorColor: widget.cursorColor,
backgroundCursorColor: widget.backgroundCursorColor,
showCursor: EditableText.debugDeterministicCursor ? ValueNotifier<bool>(true) : _showCursor, showCursor: EditableText.debugDeterministicCursor ? ValueNotifier<bool>(true) : _showCursor,
hasFocus: _hasFocus, hasFocus: _hasFocus,
maxLines: widget.maxLines, maxLines: widget.maxLines,
...@@ -1034,6 +1119,7 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -1034,6 +1119,7 @@ class _Editable extends LeafRenderObjectWidget {
this.textSpan, this.textSpan,
this.value, this.value,
this.cursorColor, this.cursorColor,
this.backgroundCursorColor,
this.showCursor, this.showCursor,
this.hasFocus, this.hasFocus,
this.maxLines, this.maxLines,
...@@ -1060,6 +1146,7 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -1060,6 +1146,7 @@ class _Editable extends LeafRenderObjectWidget {
final TextSpan textSpan; final TextSpan textSpan;
final TextEditingValue value; final TextEditingValue value;
final Color cursorColor; final Color cursorColor;
final Color backgroundCursorColor;
final ValueNotifier<bool> showCursor; final ValueNotifier<bool> showCursor;
final bool hasFocus; final bool hasFocus;
final int maxLines; final int maxLines;
...@@ -1084,6 +1171,7 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -1084,6 +1171,7 @@ class _Editable extends LeafRenderObjectWidget {
return RenderEditable( return RenderEditable(
text: textSpan, text: textSpan,
cursorColor: cursorColor, cursorColor: cursorColor,
backgroundCursorColor: backgroundCursorColor,
showCursor: showCursor, showCursor: showCursor,
hasFocus: hasFocus, hasFocus: hasFocus,
maxLines: maxLines, maxLines: maxLines,
......
// Copyright 2017 The Chromium Authors. All rights reserved. // Copyright 2017 The Chromium Authors. All rights reserved.
// 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 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import '../rendering/mock_canvas.dart'; import '../rendering/mock_canvas.dart';
import '../rendering/recording_canvas.dart'; import '../rendering/recording_canvas.dart';
...@@ -24,6 +26,10 @@ class FakeEditableTextState extends TextSelectionDelegate { ...@@ -24,6 +26,10 @@ class FakeEditableTextState extends TextSelectionDelegate {
} }
void main() { void main() {
final TextEditingController controller = TextEditingController();
const TextStyle textStyle = TextStyle();
test('editable intrinsics', () { test('editable intrinsics', () {
final TextSelectionDelegate delegate = FakeEditableTextState(); final TextSelectionDelegate delegate = FakeEditableTextState();
final RenderEditable editable = RenderEditable( final RenderEditable editable = RenderEditable(
...@@ -91,4 +97,68 @@ void main() { ...@@ -91,4 +97,68 @@ void main() {
paints..clipRect(rect: Rect.fromLTRB(0.0, 0.0, 1000.0, 10.0)) paints..clipRect(rect: Rect.fromLTRB(0.0, 0.0, 1000.0, 10.0))
); );
}); });
RenderEditable findRenderEditable(WidgetTester tester) {
final RenderObject root = tester.renderObject(find.byType(EditableText));
expect(root, isNotNull);
RenderEditable renderEditable;
void recursiveFinder(RenderObject child) {
if (child is RenderEditable) {
renderEditable = child;
return;
}
child.visitChildren(recursiveFinder);
}
root.visitChildren(recursiveFinder);
expect(renderEditable, isNotNull);
return renderEditable;
}
testWidgets('Floating cursor is painted', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
const String text = 'hello world this is fun and cool and awesome!';
controller.text = text;
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
controller: controller,
focusNode: focusNode,
style: textStyle,
),
),
),
);
await tester.tap(find.byType(EditableText));
final RenderEditable editable = findRenderEditable(tester);
editable.selection = const TextSelection(baseOffset: 29, extentOffset: 29);
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Start));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(20, 20)));
await tester.pump();
expect(find.byType(EditableText), paints..rrect(
rrect: RRect.fromRectAndRadius(Rect.fromLTRB(464.5, 0, 467.5, 16.0), const Radius.circular(1.0)), color: const Color(0xff4285f4))
);
// Moves the cursor right a few characters.
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(-250, 20)));
expect(find.byType(EditableText), paints..rrect(
rrect: RRect.fromRectAndRadius(Rect.fromLTRB(194.5, 0, 197.5, 16.0), const Radius.circular(1.0)), color: const Color(0xff4285f4))
);
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End));
await tester.pumpAndSettle();
debugDefaultTargetPlatformOverride = null;
});
} }
...@@ -1024,16 +1024,49 @@ class _RectPaintPredicate extends _OneParameterPaintPredicate<Rect> { ...@@ -1024,16 +1024,49 @@ class _RectPaintPredicate extends _OneParameterPaintPredicate<Rect> {
); );
} }
class _RRectPaintPredicate extends _OneParameterPaintPredicate<RRect> { class _RRectPaintPredicate extends _DrawCommandPaintPredicate {
_RRectPaintPredicate({ RRect rrect, Color color, double strokeWidth, bool hasMaskFilter, PaintingStyle style }) : super( _RRectPaintPredicate({ this.rrect, Color color, double strokeWidth, bool hasMaskFilter, PaintingStyle style }) : super(
#drawRRect, #drawRRect,
'a rounded rectangle', 'a rounded rectangle',
expected: rrect, 2,
1,
color: color, color: color,
strokeWidth: strokeWidth, strokeWidth: strokeWidth,
hasMaskFilter: hasMaskFilter, hasMaskFilter: hasMaskFilter,
style: style, style: style
); );
final RRect rrect;
@override
void verifyArguments(List<dynamic> arguments) {
super.verifyArguments(arguments);
const double eps = .0001;
final RRect actual = arguments[0];
if (rrect != null &&
((actual.left - rrect.left).abs() > eps ||
(actual.right - rrect.right).abs() > eps ||
(actual.top - rrect.top).abs() > eps ||
(actual.bottom - rrect.bottom).abs() > eps ||
(actual.blRadiusX - rrect.blRadiusX).abs() > eps ||
(actual.blRadiusY - rrect.blRadiusY).abs() > eps ||
(actual.brRadiusX - rrect.brRadiusX).abs() > eps ||
(actual.brRadiusY - rrect.brRadiusY).abs() > eps ||
(actual.tlRadiusX - rrect.tlRadiusX).abs() > eps ||
(actual.tlRadiusY - rrect.tlRadiusY).abs() > eps ||
(actual.trRadiusX - rrect.trRadiusX).abs() > eps ||
(actual.trRadiusY - rrect.trRadiusY).abs() > eps)) {
throw 'It called $methodName with RRect, $actual, which was not exactly the expected RRect ($rrect).';
}
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
if (rrect != null) {
description.add('RRect: $rrect');
}
}
} }
class _DRRectPaintPredicate extends _TwoParameterPaintPredicate<RRect, RRect> { class _DRRectPaintPredicate extends _TwoParameterPaintPredicate<RRect, RRect> {
......
...@@ -26,6 +26,7 @@ void main() { ...@@ -26,6 +26,7 @@ void main() {
controller: scrollController, controller: scrollController,
children: <Widget>[ children: <Widget>[
EditableText( EditableText(
backgroundCursorColor: Colors.grey,
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
style: textStyle, style: textStyle,
...@@ -67,6 +68,7 @@ void main() { ...@@ -67,6 +68,7 @@ void main() {
height: 200.0, height: 200.0,
), ),
EditableText( EditableText(
backgroundCursorColor: Colors.grey,
scrollPadding: const EdgeInsets.all(50.0), scrollPadding: const EdgeInsets.all(50.0),
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
...@@ -112,6 +114,7 @@ void main() { ...@@ -112,6 +114,7 @@ void main() {
height: 350.0, height: 350.0,
), ),
EditableText( EditableText(
backgroundCursorColor: Colors.grey,
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
style: textStyle, style: textStyle,
...@@ -161,6 +164,7 @@ void main() { ...@@ -161,6 +164,7 @@ void main() {
height: 350.0, height: 350.0,
), ),
EditableText( EditableText(
backgroundCursorColor: Colors.grey,
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
style: textStyle, style: textStyle,
...@@ -247,6 +251,7 @@ void main() { ...@@ -247,6 +251,7 @@ void main() {
controller: scrollController, controller: scrollController,
children: <Widget>[ children: <Widget>[
EditableText( EditableText(
backgroundCursorColor: Colors.grey,
maxLines: null, // multi-line maxLines: null, // multi-line
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
...@@ -302,6 +307,7 @@ void main() { ...@@ -302,6 +307,7 @@ void main() {
height: 200.0, height: 200.0,
), ),
EditableText( EditableText(
backgroundCursorColor: Colors.grey,
scrollPadding: const EdgeInsets.only(bottom: 300.0), scrollPadding: const EdgeInsets.only(bottom: 300.0),
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
......
...@@ -42,6 +42,7 @@ void main() { ...@@ -42,6 +42,7 @@ void main() {
node: focusScopeNode, node: focusScopeNode,
autofocus: true, autofocus: true,
child: EditableText( child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
textInputAction: action, textInputAction: action,
...@@ -67,6 +68,7 @@ void main() { ...@@ -67,6 +68,7 @@ void main() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: EditableText( child: EditableText(
controller: controller, controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode, focusNode: focusNode,
style: textStyle, style: textStyle,
cursorColor: cursorColor, cursorColor: cursorColor,
...@@ -87,6 +89,7 @@ void main() { ...@@ -87,6 +89,7 @@ void main() {
await tester.pumpWidget(Directionality( await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: EditableText( child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
style: textStyle, style: textStyle,
...@@ -111,6 +114,7 @@ void main() { ...@@ -111,6 +114,7 @@ void main() {
autofocus: true, autofocus: true,
child: EditableText( child: EditableText(
controller: controller, controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode, focusNode: focusNode,
style: textStyle, style: textStyle,
cursorColor: cursorColor, cursorColor: cursorColor,
...@@ -272,6 +276,7 @@ void main() { ...@@ -272,6 +276,7 @@ void main() {
autofocus: true, autofocus: true,
child: EditableText( child: EditableText(
controller: controller, controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode, focusNode: focusNode,
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
style: textStyle, style: textStyle,
...@@ -301,6 +306,7 @@ void main() { ...@@ -301,6 +306,7 @@ void main() {
autofocus: true, autofocus: true,
child: EditableText( child: EditableText(
controller: controller, controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode, focusNode: focusNode,
maxLines: null, maxLines: null,
style: textStyle, style: textStyle,
...@@ -329,6 +335,7 @@ void main() { ...@@ -329,6 +335,7 @@ void main() {
node: focusScopeNode, node: focusScopeNode,
autofocus: true, autofocus: true,
child: EditableText( child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
maxLines: null, maxLines: null,
...@@ -361,6 +368,7 @@ void main() { ...@@ -361,6 +368,7 @@ void main() {
node: focusScopeNode, node: focusScopeNode,
autofocus: true, autofocus: true,
child: EditableText( child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
keyboardType: TextInputType.phone, keyboardType: TextInputType.phone,
...@@ -392,6 +400,7 @@ void main() { ...@@ -392,6 +400,7 @@ void main() {
node: focusScopeNode, node: focusScopeNode,
autofocus: true, autofocus: true,
child: EditableText( child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
maxLines: 3, // Sets multiline keyboard implicitly. maxLines: 3, // Sets multiline keyboard implicitly.
...@@ -422,6 +431,7 @@ void main() { ...@@ -422,6 +431,7 @@ void main() {
node: focusScopeNode, node: focusScopeNode,
autofocus: true, autofocus: true,
child: EditableText( child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
maxLines: 1, // Sets text keyboard implicitly. maxLines: 1, // Sets text keyboard implicitly.
...@@ -451,6 +461,7 @@ void main() { ...@@ -451,6 +461,7 @@ void main() {
String changedValue; String changedValue;
final Widget widget = MaterialApp( final Widget widget = MaterialApp(
home: EditableText( home: EditableText(
backgroundCursorColor: Colors.grey,
key: editableTextKey, key: editableTextKey,
controller: TextEditingController(), controller: TextEditingController(),
focusNode: FocusNode(), focusNode: FocusNode(),
...@@ -494,6 +505,7 @@ void main() { ...@@ -494,6 +505,7 @@ void main() {
home: RepaintBoundary( home: RepaintBoundary(
key: const ValueKey<int>(1), key: const ValueKey<int>(1),
child: EditableText( child: EditableText(
backgroundCursorColor: Colors.grey,
key: editableTextKey, key: editableTextKey,
controller: TextEditingController(), controller: TextEditingController(),
focusNode: FocusNode(), focusNode: FocusNode(),
...@@ -544,6 +556,7 @@ void main() { ...@@ -544,6 +556,7 @@ void main() {
home: RepaintBoundary( home: RepaintBoundary(
key: const ValueKey<int>(1), key: const ValueKey<int>(1),
child: EditableText( child: EditableText(
backgroundCursorColor: Colors.grey,
key: editableTextKey, key: editableTextKey,
controller: TextEditingController(), controller: TextEditingController(),
focusNode: FocusNode(), focusNode: FocusNode(),
...@@ -594,6 +607,7 @@ void main() { ...@@ -594,6 +607,7 @@ void main() {
final Widget widget = MaterialApp( final Widget widget = MaterialApp(
home: EditableText( home: EditableText(
backgroundCursorColor: Colors.grey,
key: editableTextKey, key: editableTextKey,
controller: TextEditingController(), controller: TextEditingController(),
focusNode: focusNode, focusNode: focusNode,
...@@ -628,6 +642,7 @@ void main() { ...@@ -628,6 +642,7 @@ void main() {
final Widget widget = MaterialApp( final Widget widget = MaterialApp(
home: EditableText( home: EditableText(
backgroundCursorColor: Colors.grey,
key: editableTextKey, key: editableTextKey,
controller: TextEditingController(), controller: TextEditingController(),
focusNode: focusNode, focusNode: focusNode,
...@@ -669,6 +684,7 @@ void main() { ...@@ -669,6 +684,7 @@ void main() {
final Widget widget = MaterialApp( final Widget widget = MaterialApp(
home: EditableText( home: EditableText(
backgroundCursorColor: Colors.grey,
key: editableTextKey, key: editableTextKey,
controller: TextEditingController(), controller: TextEditingController(),
focusNode: focusNode, focusNode: focusNode,
...@@ -713,6 +729,7 @@ void main() { ...@@ -713,6 +729,7 @@ void main() {
final Widget widget = MaterialApp( final Widget widget = MaterialApp(
home: EditableText( home: EditableText(
backgroundCursorColor: Colors.grey,
key: editableTextKey, key: editableTextKey,
controller: TextEditingController(), controller: TextEditingController(),
focusNode: focusNode, focusNode: focusNode,
...@@ -757,6 +774,7 @@ void main() { ...@@ -757,6 +774,7 @@ void main() {
final Widget widget = MaterialApp( final Widget widget = MaterialApp(
home: EditableText( home: EditableText(
backgroundCursorColor: Colors.grey,
key: editableTextKey, key: editableTextKey,
controller: TextEditingController(), controller: TextEditingController(),
focusNode: focusNode, focusNode: focusNode,
...@@ -801,6 +819,7 @@ testWidgets( ...@@ -801,6 +819,7 @@ testWidgets(
final Widget widget = MaterialApp( final Widget widget = MaterialApp(
home: EditableText( home: EditableText(
backgroundCursorColor: Colors.grey,
key: editableTextKey, key: editableTextKey,
controller: TextEditingController(), controller: TextEditingController(),
focusNode: focusNode, focusNode: focusNode,
...@@ -853,6 +872,7 @@ testWidgets( ...@@ -853,6 +872,7 @@ testWidgets(
child: Center( child: Center(
child: Material( child: Material(
child: EditableText( child: EditableText(
backgroundCursorColor: Colors.grey,
key: editableTextKey, key: editableTextKey,
controller: currentController, controller: currentController,
focusNode: FocusNode(), focusNode: FocusNode(),
...@@ -912,6 +932,7 @@ testWidgets( ...@@ -912,6 +932,7 @@ testWidgets(
node: focusScopeNode, node: focusScopeNode,
autofocus: true, autofocus: true,
child: EditableText( child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
style: textStyle, style: textStyle,
...@@ -952,6 +973,7 @@ testWidgets( ...@@ -952,6 +973,7 @@ testWidgets(
child: FocusScope( child: FocusScope(
node: focusScopeNode, node: focusScopeNode,
child: EditableText( child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
style: textStyle, style: textStyle,
...@@ -992,6 +1014,7 @@ testWidgets( ...@@ -992,6 +1014,7 @@ testWidgets(
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: EditableText( home: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller, controller: controller,
selectionControls: materialTextSelectionControls, selectionControls: materialTextSelectionControls,
focusNode: focusNode, focusNode: focusNode,
...@@ -1034,6 +1057,7 @@ testWidgets( ...@@ -1034,6 +1057,7 @@ testWidgets(
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
home: EditableText( home: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
style: textStyle, style: textStyle,
...@@ -1109,6 +1133,7 @@ testWidgets( ...@@ -1109,6 +1133,7 @@ testWidgets(
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
home: EditableText( home: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
style: textStyle, style: textStyle,
...@@ -1198,6 +1223,7 @@ testWidgets( ...@@ -1198,6 +1223,7 @@ testWidgets(
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
home: EditableText( home: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
style: textStyle, style: textStyle,
...@@ -1297,6 +1323,7 @@ testWidgets( ...@@ -1297,6 +1323,7 @@ testWidgets(
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
home: EditableText( home: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
style: textStyle, style: textStyle,
...@@ -1395,6 +1422,7 @@ testWidgets( ...@@ -1395,6 +1422,7 @@ testWidgets(
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
home: EditableText( home: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
style: textStyle, style: textStyle,
...@@ -1490,6 +1518,7 @@ testWidgets( ...@@ -1490,6 +1518,7 @@ testWidgets(
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
home: EditableText( home: EditableText(
backgroundCursorColor: Colors.grey,
obscureText: true, obscureText: true,
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
...@@ -1536,6 +1565,7 @@ testWidgets( ...@@ -1536,6 +1565,7 @@ testWidgets(
MockTextSelectionControls controls, WidgetTester tester) { MockTextSelectionControls controls, WidgetTester tester) {
return tester.pumpWidget(MaterialApp( return tester.pumpWidget(MaterialApp(
home: EditableText( home: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
style: textStyle, style: textStyle,
...@@ -1743,6 +1773,7 @@ testWidgets( ...@@ -1743,6 +1773,7 @@ testWidgets(
node: focusScopeNode, node: focusScopeNode,
autofocus: true, autofocus: true,
child: EditableText( child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
autofocus: true, autofocus: true,
...@@ -1756,6 +1787,157 @@ testWidgets( ...@@ -1756,6 +1787,157 @@ testWidgets(
expect(controller.selection.isCollapsed, true); expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, text.length); expect(controller.selection.baseOffset, text.length);
}); });
RenderEditable findRenderEditable(WidgetTester tester) {
final RenderObject root = tester.renderObject(find.byType(EditableText));
expect(root, isNotNull);
RenderEditable renderEditable;
void recursiveFinder(RenderObject child) {
if (child is RenderEditable) {
renderEditable = child;
return;
}
child.visitChildren(recursiveFinder);
}
root.visitChildren(recursiveFinder);
expect(renderEditable, isNotNull);
return renderEditable;
}
testWidgets('Updating the floating cursor correctly moves the cursor', (WidgetTester tester) async {
const String text = 'hello world this is fun and cool and awesome!';
controller.text = text;
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
),
),
);
await tester.tap(find.byType(EditableText));
final RenderEditable renderEditable = findRenderEditable(tester);
renderEditable.selection = const TextSelection(baseOffset: 29, extentOffset: 29);
expect(controller.selection.baseOffset, 29);
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Start));
expect(controller.selection.baseOffset, 29);
// Sets the origin.
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(20, 20)));
expect(controller.selection.baseOffset, 29);
// Moves the cursor right a few characters.
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(-250, 20)));
// But we have not yet set the offset because the user is not done placing the cursor.
expect(controller.selection.baseOffset, 29);
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End));
await tester.pumpAndSettle();
// The cursor has been set.
expect(controller.selection.baseOffset, 10);
});
testWidgets('Cursor gets placed correctly after going out of bounds', (WidgetTester tester) async {
const String text = 'hello world this is fun and cool and awesome!';
controller.text = text;
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
),
),
);
await tester.tap(find.byType(EditableText));
final RenderEditable renderEditable = findRenderEditable(tester);
renderEditable.selection = const TextSelection(baseOffset: 29, extentOffset: 29);
expect(controller.selection.baseOffset, 29);
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Start));
expect(controller.selection.baseOffset, 29);
// Sets the origin.
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(20, 20)));
expect(controller.selection.baseOffset, 29);
// Moves the cursor super far right
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(2090, 20)));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(2100, 20)));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(2090, 20)));
// After peaking the cursor, we move in the opposite direction.
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(1400, 20)));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End));
await tester.pumpAndSettle();
// The cursor has been set.
expect(controller.selection.baseOffset, 8);
// Go in the other direction.
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Start));
// Sets the origin.
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(20, 20)));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(-5000, 20)));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(-5010, 20)));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(-5000, 20)));
// Move back in the opposite direction only a few hundred.
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update,
offset: const Offset(-4850, 20)));
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 11);
});
} }
class MockTextSelectionControls extends Mock implements TextSelectionControls {} class MockTextSelectionControls extends Mock implements TextSelectionControls {}
...@@ -1769,6 +1951,7 @@ class CustomStyleEditableText extends EditableText { ...@@ -1769,6 +1951,7 @@ class CustomStyleEditableText extends EditableText {
}) : super( }) : super(
controller: controller, controller: controller,
cursorColor: cursorColor, cursorColor: cursorColor,
backgroundCursorColor: Colors.grey,
focusNode: focusNode, focusNode: focusNode,
style: style, style: style,
); );
......
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