Unverified Commit 0d4eb5ea authored by yim's avatar yim Committed by GitHub

Changes the regular cursor to a floating cursor when a long press occurs. (#138479)

This PR changes the regular cursor to a floating cursor when a long press occurs.

This is a new feature. Fixes  #89228
parent 83ac7605
...@@ -30,6 +30,13 @@ const EdgeInsets _kFloatingCursorSizeIncrease = EdgeInsets.symmetric(horizontal: ...@@ -30,6 +30,13 @@ const EdgeInsets _kFloatingCursorSizeIncrease = EdgeInsets.symmetric(horizontal:
// The corner radius of the floating cursor in pixels. // The corner radius of the floating cursor in pixels.
const Radius _kFloatingCursorRadius = Radius.circular(1.0); const Radius _kFloatingCursorRadius = Radius.circular(1.0);
// This constant represents the shortest squared distance required between the floating cursor
// and the regular cursor when both are present in the text field.
// If the squared distance between the two cursors is less than this value,
// it's not necessary to display both cursors at the same time.
// This behavior is consistent with the one observed in iOS UITextField.
const double _kShortestDistanceSquaredWithFloatingAndRegularCursors = 15.0 * 15.0;
/// Represents the coordinates of the point in a selection, and the text /// Represents the coordinates of the point in a selection, and the text
/// direction at that point, relative to top left of the [RenderEditable] that /// direction at that point, relative to top left of the [RenderEditable] that
/// holds the selection. /// holds the selection.
...@@ -2360,19 +2367,35 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -2360,19 +2367,35 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
// difference in the rendering position and the raw offset value. // difference in the rendering position and the raw offset value.
Offset _relativeOrigin = Offset.zero; Offset _relativeOrigin = Offset.zero;
Offset? _previousOffset; Offset? _previousOffset;
bool _shouldResetOrigin = true;
bool _resetOriginOnLeft = false; bool _resetOriginOnLeft = false;
bool _resetOriginOnRight = false; bool _resetOriginOnRight = false;
bool _resetOriginOnTop = false; bool _resetOriginOnTop = false;
bool _resetOriginOnBottom = false; bool _resetOriginOnBottom = false;
double? _resetFloatingCursorAnimationValue; double? _resetFloatingCursorAnimationValue;
static Offset _calculateAdjustedCursorOffset(Offset offset, Rect boundingRects) {
final double adjustedX = clampDouble(offset.dx, boundingRects.left, boundingRects.right);
final double adjustedY = clampDouble(offset.dy, boundingRects.top, boundingRects.bottom);
return Offset(adjustedX, adjustedY);
}
/// Returns the position within the text field closest to the raw cursor offset. /// Returns the position within the text field closest to the raw cursor offset.
Offset calculateBoundedFloatingCursorOffset(Offset rawCursorOffset) { Offset calculateBoundedFloatingCursorOffset(Offset rawCursorOffset, {bool? shouldResetOrigin}) {
Offset deltaPosition = Offset.zero; Offset deltaPosition = Offset.zero;
final double topBound = -floatingCursorAddedMargin.top; final double topBound = -floatingCursorAddedMargin.top;
final double bottomBound = _textPainter.height - preferredLineHeight + floatingCursorAddedMargin.bottom; final double bottomBound = math.min(size.height, _textPainter.height) - preferredLineHeight + floatingCursorAddedMargin.bottom;
final double leftBound = -floatingCursorAddedMargin.left; final double leftBound = -floatingCursorAddedMargin.left;
final double rightBound = _textPainter.width + floatingCursorAddedMargin.right; final double rightBound = math.min(size.width, _textPainter.width) + floatingCursorAddedMargin.right;
final Rect boundingRects = Rect.fromLTRB(leftBound, topBound, rightBound, bottomBound);
if (shouldResetOrigin != null) {
_shouldResetOrigin = shouldResetOrigin;
}
if (!_shouldResetOrigin) {
return _calculateAdjustedCursorOffset(rawCursorOffset, boundingRects);
}
if (_previousOffset != null) { if (_previousOffset != null) {
deltaPosition = rawCursorOffset - _previousOffset!; deltaPosition = rawCursorOffset - _previousOffset!;
...@@ -2381,34 +2404,32 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -2381,34 +2404,32 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
// If the raw cursor offset has gone off an edge, we want to reset the relative // 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. // origin of the dragging when the user drags back into the field.
if (_resetOriginOnLeft && deltaPosition.dx > 0) { if (_resetOriginOnLeft && deltaPosition.dx > 0) {
_relativeOrigin = Offset(rawCursorOffset.dx - leftBound, _relativeOrigin.dy); _relativeOrigin = Offset(rawCursorOffset.dx - boundingRects.left, _relativeOrigin.dy);
_resetOriginOnLeft = false; _resetOriginOnLeft = false;
} else if (_resetOriginOnRight && deltaPosition.dx < 0) { } else if (_resetOriginOnRight && deltaPosition.dx < 0) {
_relativeOrigin = Offset(rawCursorOffset.dx - rightBound, _relativeOrigin.dy); _relativeOrigin = Offset(rawCursorOffset.dx - boundingRects.right, _relativeOrigin.dy);
_resetOriginOnRight = false; _resetOriginOnRight = false;
} }
if (_resetOriginOnTop && deltaPosition.dy > 0) { if (_resetOriginOnTop && deltaPosition.dy > 0) {
_relativeOrigin = Offset(_relativeOrigin.dx, rawCursorOffset.dy - topBound); _relativeOrigin = Offset(_relativeOrigin.dx, rawCursorOffset.dy - boundingRects.top);
_resetOriginOnTop = false; _resetOriginOnTop = false;
} else if (_resetOriginOnBottom && deltaPosition.dy < 0) { } else if (_resetOriginOnBottom && deltaPosition.dy < 0) {
_relativeOrigin = Offset(_relativeOrigin.dx, rawCursorOffset.dy - bottomBound); _relativeOrigin = Offset(_relativeOrigin.dx, rawCursorOffset.dy - boundingRects.bottom);
_resetOriginOnBottom = false; _resetOriginOnBottom = false;
} }
final double currentX = rawCursorOffset.dx - _relativeOrigin.dx; final double currentX = rawCursorOffset.dx - _relativeOrigin.dx;
final double currentY = rawCursorOffset.dy - _relativeOrigin.dy; final double currentY = rawCursorOffset.dy - _relativeOrigin.dy;
final double adjustedX = math.min(math.max(currentX, leftBound), rightBound); final Offset adjustedOffset = _calculateAdjustedCursorOffset(Offset(currentX, currentY), boundingRects);
final double adjustedY = math.min(math.max(currentY, topBound), bottomBound);
final Offset adjustedOffset = Offset(adjustedX, adjustedY);
if (currentX < leftBound && deltaPosition.dx < 0) { if (currentX < boundingRects.left && deltaPosition.dx < 0) {
_resetOriginOnLeft = true; _resetOriginOnLeft = true;
} else if (currentX > rightBound && deltaPosition.dx > 0) { } else if (currentX > boundingRects.right && deltaPosition.dx > 0) {
_resetOriginOnRight = true; _resetOriginOnRight = true;
} }
if (currentY < topBound && deltaPosition.dy < 0) { if (currentY < boundingRects.top && deltaPosition.dy < 0) {
_resetOriginOnTop = true; _resetOriginOnTop = true;
} else if (currentY > bottomBound && deltaPosition.dy > 0) { } else if (currentY > boundingRects.bottom && deltaPosition.dy > 0) {
_resetOriginOnBottom = true; _resetOriginOnBottom = true;
} }
...@@ -2420,9 +2441,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -2420,9 +2441,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
/// Sets the screen position of the floating cursor and the text position /// Sets the screen position of the floating cursor and the text position
/// closest to the cursor. /// closest to the cursor.
void setFloatingCursor(FloatingCursorDragState state, Offset boundedOffset, TextPosition lastTextPosition, { double? resetLerpValue }) { void setFloatingCursor(FloatingCursorDragState state, Offset boundedOffset, TextPosition lastTextPosition, { double? resetLerpValue }) {
if (state == FloatingCursorDragState.Start) { if (state == FloatingCursorDragState.End) {
_relativeOrigin = Offset.zero; _relativeOrigin = Offset.zero;
_previousOffset = null; _previousOffset = null;
_shouldResetOrigin = true;
_resetOriginOnBottom = false; _resetOriginOnBottom = false;
_resetOriginOnTop = false; _resetOriginOnTop = false;
_resetOriginOnRight = false; _resetOriginOnRight = false;
...@@ -2898,6 +2920,12 @@ class _CaretPainter extends RenderEditablePainter { ...@@ -2898,6 +2920,12 @@ class _CaretPainter extends RenderEditablePainter {
void paintRegularCursor(Canvas canvas, RenderEditable renderEditable, Color caretColor, TextPosition textPosition) { void paintRegularCursor(Canvas canvas, RenderEditable renderEditable, Color caretColor, TextPosition textPosition) {
final Rect integralRect = renderEditable.getLocalRectForCaret(textPosition); final Rect integralRect = renderEditable.getLocalRectForCaret(textPosition);
if (shouldPaint) { if (shouldPaint) {
if (floatingCursorRect != null) {
final double distanceSquared = (floatingCursorRect!.center - integralRect.center).distanceSquared;
if (distanceSquared < _kShortestDistanceSquaredWithFloatingAndRegularCursors) {
return;
}
}
final Radius? radius = cursorRadius; final Radius? radius = cursorRadius;
caretPaint.color = caretColor; caretPaint.color = caretColor;
if (radius == null) { if (radius == null) {
......
...@@ -744,12 +744,18 @@ class RawFloatingCursorPoint { ...@@ -744,12 +744,18 @@ class RawFloatingCursorPoint {
/// [FloatingCursorDragState.Update]. /// [FloatingCursorDragState.Update].
RawFloatingCursorPoint({ RawFloatingCursorPoint({
this.offset, this.offset,
this.startLocation,
required this.state, required this.state,
}) : assert(state != FloatingCursorDragState.Update || offset != null); }) : assert(state != FloatingCursorDragState.Update || offset != null);
/// The raw position of the floating cursor as determined by the iOS sdk. /// The raw position of the floating cursor as determined by the iOS sdk.
final Offset? offset; final Offset? offset;
/// Represents the starting location when initiating a floating cursor via long press.
/// This is a tuple where the first item is the local offset and the second item is the new caret position.
/// This is only non-null when a floating cursor is started.
final (Offset, TextPosition)? startLocation;
/// The state of the floating cursor. /// The state of the floating cursor.
final FloatingCursorDragState state; final FloatingCursorDragState state;
} }
......
...@@ -3162,7 +3162,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -3162,7 +3162,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
// The original position of the caret on FloatingCursorDragState.start. // The original position of the caret on FloatingCursorDragState.start.
Rect? _startCaretRect; Offset? _startCaretCenter;
// The most recent text position as determined by the location of the floating // The most recent text position as determined by the location of the floating
// cursor. // cursor.
...@@ -3197,15 +3197,26 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -3197,15 +3197,26 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// we cache the position. // we cache the position.
_pointOffsetOrigin = point.offset; _pointOffsetOrigin = point.offset;
final TextPosition currentTextPosition = TextPosition(offset: renderEditable.selection!.baseOffset, affinity: renderEditable.selection!.affinity); final Offset startCaretCenter;
_startCaretRect = renderEditable.getLocalRectForCaret(currentTextPosition); final TextPosition currentTextPosition;
final bool shouldResetOrigin;
// Only non-null when starting a floating cursor via long press.
if (point.startLocation != null) {
shouldResetOrigin = false;
(startCaretCenter, currentTextPosition) = point.startLocation!;
} else {
shouldResetOrigin = true;
currentTextPosition = TextPosition(offset: renderEditable.selection!.baseOffset, affinity: renderEditable.selection!.affinity);
startCaretCenter = renderEditable.getLocalRectForCaret(currentTextPosition).center;
}
_lastBoundedOffset = _startCaretRect!.center - _floatingCursorOffset; _startCaretCenter = startCaretCenter;
_lastBoundedOffset = renderEditable.calculateBoundedFloatingCursorOffset(_startCaretCenter! - _floatingCursorOffset, shouldResetOrigin: shouldResetOrigin);
_lastTextPosition = currentTextPosition; _lastTextPosition = currentTextPosition;
renderEditable.setFloatingCursor(point.state, _lastBoundedOffset!, _lastTextPosition!); renderEditable.setFloatingCursor(point.state, _lastBoundedOffset!, _lastTextPosition!);
case FloatingCursorDragState.Update: case FloatingCursorDragState.Update:
final Offset centeredPoint = point.offset! - _pointOffsetOrigin!; final Offset centeredPoint = point.offset! - _pointOffsetOrigin!;
final Offset rawCursorOffset = _startCaretRect!.center + centeredPoint - _floatingCursorOffset; final Offset rawCursorOffset = _startCaretCenter! + centeredPoint - _floatingCursorOffset;
_lastBoundedOffset = renderEditable.calculateBoundedFloatingCursorOffset(rawCursorOffset); _lastBoundedOffset = renderEditable.calculateBoundedFloatingCursorOffset(rawCursorOffset);
_lastTextPosition = renderEditable.getPositionForPoint(renderEditable.localToGlobal(_lastBoundedOffset! + _floatingCursorOffset)); _lastTextPosition = renderEditable.getPositionForPoint(renderEditable.localToGlobal(_lastBoundedOffset! + _floatingCursorOffset));
...@@ -3245,7 +3256,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -3245,7 +3256,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// The cause is technically the force cursor, but the cause is listed as tap as the desired functionality is the same. // The cause is technically the force cursor, but the cause is listed as tap as the desired functionality is the same.
_handleSelectionChanged(TextSelection.fromPosition(_lastTextPosition!), SelectionChangedCause.forcePress); _handleSelectionChanged(TextSelection.fromPosition(_lastTextPosition!), SelectionChangedCause.forcePress);
} }
_startCaretRect = null; _startCaretCenter = null;
_lastTextPosition = null; _lastTextPosition = null;
_pointOffsetOrigin = null; _pointOffsetOrigin = null;
_lastBoundedOffset = null; _lastBoundedOffset = null;
......
...@@ -2453,6 +2453,19 @@ class TextSelectionGestureDetectorBuilder { ...@@ -2453,6 +2453,19 @@ class TextSelectionGestureDetectorBuilder {
from: details.globalPosition, from: details.globalPosition,
cause: SelectionChangedCause.longPress, cause: SelectionChangedCause.longPress,
); );
// Show the floating cursor.
final RawFloatingCursorPoint cursorPoint = RawFloatingCursorPoint(
state: FloatingCursorDragState.Start,
startLocation: (
renderEditable.globalToLocal(details.globalPosition),
TextPosition(
offset: editableText.textEditingValue.selection.baseOffset,
affinity: editableText.textEditingValue.selection.affinity,
),
),
offset: Offset.zero,
);
editableText.updateFloatingCursor(cursorPoint);
} }
case TargetPlatform.android: case TargetPlatform.android:
case TargetPlatform.fuchsia: case TargetPlatform.fuchsia:
...@@ -2488,7 +2501,6 @@ class TextSelectionGestureDetectorBuilder { ...@@ -2488,7 +2501,6 @@ class TextSelectionGestureDetectorBuilder {
0.0, 0.0,
_scrollPosition - _dragStartScrollOffset, _scrollPosition - _dragStartScrollOffset,
); );
switch (defaultTargetPlatform) { switch (defaultTargetPlatform) {
case TargetPlatform.iOS: case TargetPlatform.iOS:
case TargetPlatform.macOS: case TargetPlatform.macOS:
...@@ -2503,6 +2515,12 @@ class TextSelectionGestureDetectorBuilder { ...@@ -2503,6 +2515,12 @@ class TextSelectionGestureDetectorBuilder {
from: details.globalPosition, from: details.globalPosition,
cause: SelectionChangedCause.longPress, cause: SelectionChangedCause.longPress,
); );
// Update the floating cursor.
final RawFloatingCursorPoint cursorPoint = RawFloatingCursorPoint(
state: FloatingCursorDragState.Update,
offset: details.offsetFromOrigin,
);
editableText.updateFloatingCursor(cursorPoint);
} }
case TargetPlatform.android: case TargetPlatform.android:
case TargetPlatform.fuchsia: case TargetPlatform.fuchsia:
...@@ -2536,6 +2554,13 @@ class TextSelectionGestureDetectorBuilder { ...@@ -2536,6 +2554,13 @@ class TextSelectionGestureDetectorBuilder {
_longPressStartedWithoutFocus = false; _longPressStartedWithoutFocus = false;
_dragStartViewportOffset = 0.0; _dragStartViewportOffset = 0.0;
_dragStartScrollOffset = 0.0; _dragStartScrollOffset = 0.0;
if (defaultTargetPlatform == TargetPlatform.iOS && delegate.selectionEnabled && editableText.textEditingValue.selection.isCollapsed) {
// Update the floating cursor.
final RawFloatingCursorPoint cursorPoint = RawFloatingCursorPoint(
state: FloatingCursorDragState.End
);
editableText.updateFloatingCursor(cursorPoint);
}
} }
/// Handler for [TextSelectionGestureDetector.onSecondaryTap]. /// Handler for [TextSelectionGestureDetector.onSecondaryTap].
......
...@@ -12,7 +12,7 @@ import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle, Color; ...@@ -12,7 +12,7 @@ import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle, Color;
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior, PointerDeviceKind, kDoubleTapTimeout, kSecondaryMouseButton; import 'package:flutter/gestures.dart' show DragStartBehavior, PointerDeviceKind, kDoubleTapTimeout, kLongPressTimeout, kSecondaryMouseButton;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
...@@ -279,7 +279,7 @@ void main() { ...@@ -279,7 +279,7 @@ void main() {
// Long press to put the cursor after the "s". // Long press to put the cursor after the "s".
const int index = 3; const int index = 3;
await tester.longPressAt(textOffsetToPosition(tester, index)); await tester.longPressAt(textOffsetToPosition(tester, index));
await tester.pump(); await tester.pumpAndSettle();
// Double tap on the same location to select the word around the cursor. // Double tap on the same location to select the word around the cursor.
await tester.tapAt(textOffsetToPosition(tester, index)); await tester.tapAt(textOffsetToPosition(tester, index));
...@@ -329,7 +329,7 @@ void main() { ...@@ -329,7 +329,7 @@ void main() {
// Long press to put the cursor after the "s". // Long press to put the cursor after the "s".
const int index = 3; const int index = 3;
await tester.longPressAt(textOffsetToPosition(tester, index)); await tester.longPressAt(textOffsetToPosition(tester, index));
await tester.pump(); await tester.pumpAndSettle();
// Double tap on the same location to select the word around the cursor. // Double tap on the same location to select the word around the cursor.
await tester.tapAt(textOffsetToPosition(tester, index)); await tester.tapAt(textOffsetToPosition(tester, index));
...@@ -377,7 +377,7 @@ void main() { ...@@ -377,7 +377,7 @@ void main() {
// Long press to put the cursor after the "s". // Long press to put the cursor after the "s".
const int index = 3; const int index = 3;
await tester.longPressAt(textOffsetToPosition(tester, index)); await tester.longPressAt(textOffsetToPosition(tester, index));
await tester.pump(); await tester.pumpAndSettle();
// Double tap on the same location to select the word around the cursor. // Double tap on the same location to select the word around the cursor.
await tester.tapAt(textOffsetToPosition(tester, index)); await tester.tapAt(textOffsetToPosition(tester, index));
...@@ -1749,7 +1749,7 @@ void main() { ...@@ -1749,7 +1749,7 @@ void main() {
await tester.longPressAt( await tester.longPressAt(
tester.getTopRight(find.text("j'aime la poutine")), tester.getTopRight(find.text("j'aime la poutine")),
); );
await tester.pump(); await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 200)); await tester.pump(const Duration(milliseconds: 200));
Text text = tester.widget<Text>(find.text('Paste')); Text text = tester.widget<Text>(find.text('Paste'));
...@@ -1780,7 +1780,7 @@ void main() { ...@@ -1780,7 +1780,7 @@ void main() {
await tester.longPressAt( await tester.longPressAt(
tester.getTopRight(find.text("j'aime la poutine")), tester.getTopRight(find.text("j'aime la poutine")),
); );
await tester.pump(); await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 200)); await tester.pump(const Duration(milliseconds: 200));
text = tester.widget<Text>(find.text('Paste')); text = tester.widget<Text>(find.text('Paste'));
...@@ -1816,7 +1816,7 @@ void main() { ...@@ -1816,7 +1816,7 @@ void main() {
// Long press to put the cursor after the "w". // Long press to put the cursor after the "w".
const int index = 3; const int index = 3;
await tester.longPressAt(textOffsetToPosition(tester, index)); await tester.longPressAt(textOffsetToPosition(tester, index));
await tester.pump(); await tester.pumpAndSettle();
expect( expect(
controller.selection, controller.selection,
const TextSelection.collapsed(offset: index), const TextSelection.collapsed(offset: index),
...@@ -1863,7 +1863,7 @@ void main() { ...@@ -1863,7 +1863,7 @@ void main() {
// Long press to select 'Atwater' // Long press to select 'Atwater'
const int index = 3; const int index = 3;
await tester.longPressAt(textOffsetToPosition(tester, index)); await tester.longPressAt(textOffsetToPosition(tester, index));
await tester.pump(); await tester.pumpAndSettle();
expect( expect(
controller.selection, controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7), const TextSelection(baseOffset: 0, extentOffset: 7),
...@@ -1922,7 +1922,7 @@ void main() { ...@@ -1922,7 +1922,7 @@ void main() {
tester.getTopRight(find.text('readonly')), tester.getTopRight(find.text('readonly')),
); );
await tester.pump(); await tester.pumpAndSettle();
expect(find.text('Paste'), findsNothing); expect(find.text('Paste'), findsNothing);
expect(find.text('Cut'), findsNothing); expect(find.text('Cut'), findsNothing);
...@@ -1962,7 +1962,7 @@ void main() { ...@@ -1962,7 +1962,7 @@ void main() {
await tester.longPressAt( await tester.longPressAt(
tester.getTopRight(find.text("j'aime la poutine")), tester.getTopRight(find.text("j'aime la poutine")),
); );
await tester.pump(); await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 200)); await tester.pump(const Duration(milliseconds: 200));
await tester.tap(find.text('Select All')); await tester.tap(find.text('Select All'));
...@@ -2244,7 +2244,7 @@ void main() { ...@@ -2244,7 +2244,7 @@ void main() {
// Long press to select 'Atwater'. // Long press to select 'Atwater'.
const int index = 3; const int index = 3;
await tester.longPressAt(textOffsetToPosition(tester, index)); await tester.longPressAt(textOffsetToPosition(tester, index));
await tester.pump(); await tester.pumpAndSettle();
expect( expect(
controller.selection, controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7), const TextSelection(baseOffset: 0, extentOffset: 7),
...@@ -2304,7 +2304,7 @@ void main() { ...@@ -2304,7 +2304,7 @@ void main() {
// Long press to put the cursor after the "w". // Long press to put the cursor after the "w".
const int index = 3; const int index = 3;
await tester.longPressAt(textOffsetToPosition(tester, index)); await tester.longPressAt(textOffsetToPosition(tester, index));
await tester.pump(); await tester.pumpAndSettle();
expect( expect(
controller.selection, controller.selection,
const TextSelection.collapsed(offset: index), const TextSelection.collapsed(offset: index),
...@@ -2349,7 +2349,7 @@ void main() { ...@@ -2349,7 +2349,7 @@ void main() {
// Long press to put the cursor after the "w". // Long press to put the cursor after the "w".
const int index = 3; const int index = 3;
await tester.longPressAt(textOffsetToPosition(tester, index)); await tester.longPressAt(textOffsetToPosition(tester, index));
await tester.pump(); await tester.pumpAndSettle();
// Second tap doesn't select anything. // Second tap doesn't select anything.
await tester.tapAt(textOffsetToPosition(tester, index)); await tester.tapAt(textOffsetToPosition(tester, index));
...@@ -2993,7 +2993,7 @@ void main() { ...@@ -2993,7 +2993,7 @@ void main() {
await tester.tapAt(textFieldStart + const Offset(150.0, 5.0)); await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
await tester.pump(const Duration(milliseconds: 50)); await tester.pump(const Duration(milliseconds: 50));
await tester.longPressAt(textFieldStart + const Offset(150.0, 5.0)); await tester.longPressAt(textFieldStart + const Offset(150.0, 5.0));
await tester.pump(); await tester.pumpAndSettle();
// Should only have paste option when whole obscure text is selected. // Should only have paste option when whole obscure text is selected.
expect(find.text('Paste'), findsOneWidget); expect(find.text('Paste'), findsOneWidget);
...@@ -3007,7 +3007,7 @@ void main() { ...@@ -3007,7 +3007,7 @@ void main() {
await tester.pump(const Duration(milliseconds: 50)); await tester.pump(const Duration(milliseconds: 50));
// Long tap at the end. // Long tap at the end.
await tester.longPressAt(textFieldEnd + const Offset(-10.0, 5.0)); await tester.longPressAt(textFieldEnd + const Offset(-10.0, 5.0));
await tester.pump(); await tester.pumpAndSettle();
// Should have paste and select all options when collapse. // Should have paste and select all options when collapse.
expect(find.text('Paste'), findsOneWidget); expect(find.text('Paste'), findsOneWidget);
...@@ -3110,7 +3110,7 @@ void main() { ...@@ -3110,7 +3110,7 @@ void main() {
final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r' final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'
await tester.longPressAt(ePos); await tester.longPressAt(ePos);
await tester.pump(const Duration(milliseconds: 50)); await tester.pumpAndSettle(const Duration(milliseconds: 50));
expectCupertinoToolbarForCollapsedSelection(); expectCupertinoToolbarForCollapsedSelection();
...@@ -3524,7 +3524,7 @@ void main() { ...@@ -3524,7 +3524,7 @@ void main() {
final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater' final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'
await tester.longPressAt(wPos); await tester.longPressAt(wPos);
await tester.pump(const Duration(milliseconds: 50)); await tester.pumpAndSettle(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, isTrue); expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, 3); expect(controller.selection.baseOffset, 3);
...@@ -7986,7 +7986,7 @@ void main() { ...@@ -7986,7 +7986,7 @@ void main() {
final Offset textFieldStart = tester.getTopLeft(find.byKey(const Key('field0'))); final Offset textFieldStart = tester.getTopLeft(find.byKey(const Key('field0')));
await tester.longPressAt(textFieldStart + const Offset(50.0, 2.0)); await tester.longPressAt(textFieldStart + const Offset(50.0, 2.0));
await tester.pump(const Duration(milliseconds: 150)); await tester.pumpAndSettle(const Duration(milliseconds: 150));
// Tap the Select All button. // Tap the Select All button.
await tester.tapAt(textFieldStart + const Offset(20.0, 100.0)); await tester.tapAt(textFieldStart + const Offset(20.0, 100.0));
await tester.pump(const Duration(milliseconds: 300)); await tester.pump(const Duration(milliseconds: 300));
...@@ -8040,7 +8040,7 @@ void main() { ...@@ -8040,7 +8040,7 @@ void main() {
final Offset textFieldStart = tester.getTopLeft(find.byKey(const Key('field0'))); final Offset textFieldStart = tester.getTopLeft(find.byKey(const Key('field0')));
await tester.longPressAt(textFieldStart + const Offset(50.0, 2.0)); await tester.longPressAt(textFieldStart + const Offset(50.0, 2.0));
await tester.pump(const Duration(milliseconds: 150)); await tester.pumpAndSettle(const Duration(milliseconds: 150));
// Tap the Select All button. // Tap the Select All button.
await tester.tapAt(textFieldStart + const Offset(20.0, 100.0)); await tester.tapAt(textFieldStart + const Offset(20.0, 100.0));
await tester.pump(const Duration(milliseconds: 300)); await tester.pump(const Duration(milliseconds: 300));
...@@ -10094,4 +10094,45 @@ void main() { ...@@ -10094,4 +10094,45 @@ void main() {
// placeholder. // placeholder.
expect(rectWithText.height, greaterThan(100)); expect(rectWithText.height, greaterThan(100));
}); });
testWidgets('Start the floating cursor on long tap', (WidgetTester tester) async {
EditableText.debugDeterministicCursor = true;
final TextEditingController controller = TextEditingController(
text: 'abcd',
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: RepaintBoundary(
key: const ValueKey<int>(1),
child: CupertinoTextField(
autofocus: true,
controller: controller,
),
)
),
),
),
);
// Wait for autofocus.
await tester.pumpAndSettle();
final Offset textFieldCenter = tester.getCenter(find.byType(CupertinoTextField));
final TestGesture gesture = await tester.startGesture(textFieldCenter);
await tester.pump(kLongPressTimeout);
await expectLater(
find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('text_field_floating_cursor.regular_and_floating_both.cupertino.0.png'),
);
await gesture.moveTo(Offset(10, textFieldCenter.dy));
await tester.pump();
await expectLater(
find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('text_field_floating_cursor.only_floating_cursor.cupertino.0.png'),
);
await gesture.up();
EditableText.debugDeterministicCursor = false;
},
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
);
} }
...@@ -512,7 +512,7 @@ void main() { ...@@ -512,7 +512,7 @@ void main() {
// Long press to put the cursor after the "s". // Long press to put the cursor after the "s".
const int index = 3; const int index = 3;
await tester.longPressAt(textOffsetToPosition(tester, index)); await tester.longPressAt(textOffsetToPosition(tester, index));
await tester.pump(); await tester.pumpAndSettle();
// Double tap on the same location to select the word around the cursor. // Double tap on the same location to select the word around the cursor.
await tester.tapAt(textOffsetToPosition(tester, index)); await tester.tapAt(textOffsetToPosition(tester, index));
...@@ -561,7 +561,7 @@ void main() { ...@@ -561,7 +561,7 @@ void main() {
// Long press to put the cursor after the "s". // Long press to put the cursor after the "s".
const int index = 3; const int index = 3;
await tester.longPressAt(textOffsetToPosition(tester, index)); await tester.longPressAt(textOffsetToPosition(tester, index));
await tester.pump(); await tester.pumpAndSettle();
// Double tap on the same location to select the word around the cursor. // Double tap on the same location to select the word around the cursor.
await tester.tapAt(textOffsetToPosition(tester, index)); await tester.tapAt(textOffsetToPosition(tester, index));
...@@ -610,7 +610,7 @@ void main() { ...@@ -610,7 +610,7 @@ void main() {
// Long press to put the cursor after the "s". // Long press to put the cursor after the "s".
const int index = 3; const int index = 3;
await tester.longPressAt(textOffsetToPosition(tester, index)); await tester.longPressAt(textOffsetToPosition(tester, index));
await tester.pump(); await tester.pumpAndSettle();
// Double tap on the same location to select the word around the cursor. // Double tap on the same location to select the word around the cursor.
await tester.tapAt(textOffsetToPosition(tester, index)); await tester.tapAt(textOffsetToPosition(tester, index));
...@@ -1244,7 +1244,7 @@ void main() { ...@@ -1244,7 +1244,7 @@ void main() {
final Offset textfieldStart = tester.getTopLeft(find.byKey(const Key('field0'))); final Offset textfieldStart = tester.getTopLeft(find.byKey(const Key('field0')));
await tester.longPressAt(textfieldStart + const Offset(50.0, 2.0)); await tester.longPressAt(textfieldStart + const Offset(50.0, 2.0));
await tester.pump(const Duration(milliseconds: 50)); await tester.pumpAndSettle(const Duration(milliseconds: 50));
await tester.tapAt(textfieldStart + const Offset(100.0, 107.0)); await tester.tapAt(textfieldStart + const Offset(100.0, 107.0));
await tester.pump(const Duration(milliseconds: 300)); await tester.pump(const Duration(milliseconds: 300));
...@@ -1715,7 +1715,7 @@ void main() { ...@@ -1715,7 +1715,7 @@ void main() {
// Long press the 'e' to select 'def'. // Long press the 'e' to select 'def'.
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
await tester.longPressAt(ePos, pointer: 7); await tester.longPressAt(ePos, pointer: 7);
await tester.pump(); await tester.pumpAndSettle();
// 'def' is selected. // 'def' is selected.
expect(controller.selection.baseOffset, testValue.indexOf('d')); expect(controller.selection.baseOffset, testValue.indexOf('d'));
...@@ -2174,7 +2174,7 @@ void main() { ...@@ -2174,7 +2174,7 @@ void main() {
// Long press the 'e' to select 'def'. // Long press the 'e' to select 'def'.
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
await tester.longPressAt(ePos, pointer: 7); await tester.longPressAt(ePos, pointer: 7);
await tester.pump(); await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true); expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.length); expect(controller.selection.baseOffset, testValue.length);
...@@ -4209,7 +4209,7 @@ void main() { ...@@ -4209,7 +4209,7 @@ void main() {
// Long press does select text. // Long press does select text.
final Offset ePos = textOffsetToPosition(tester, 1); final Offset ePos = textOffsetToPosition(tester, 1);
await tester.longPressAt(ePos, pointer: 7); await tester.longPressAt(ePos, pointer: 7);
await tester.pump(); await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, false); expect(controller.selection.isCollapsed, false);
}); });
...@@ -4237,7 +4237,7 @@ void main() { ...@@ -4237,7 +4237,7 @@ void main() {
// Long press doesn't select text. // Long press doesn't select text.
final Offset ePos2 = textOffsetToPosition(tester, 1); final Offset ePos2 = textOffsetToPosition(tester, 1);
await tester.longPressAt(ePos2, pointer: 7); await tester.longPressAt(ePos2, pointer: 7);
await tester.pump(); await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true); expect(controller.selection.isCollapsed, true);
}); });
...@@ -4265,7 +4265,7 @@ void main() { ...@@ -4265,7 +4265,7 @@ void main() {
// Long press doesn't select text. // Long press doesn't select text.
final Offset ePos2 = textOffsetToPosition(tester, 1); final Offset ePos2 = textOffsetToPosition(tester, 1);
await tester.longPressAt(ePos2, pointer: 7); await tester.longPressAt(ePos2, pointer: 7);
await tester.pump(); await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true); expect(controller.selection.isCollapsed, true);
}); });
...@@ -4284,7 +4284,7 @@ void main() { ...@@ -4284,7 +4284,7 @@ void main() {
// Long press does select text. // Long press does select text.
final Offset bPos = textOffsetToPosition(tester, 1); final Offset bPos = textOffsetToPosition(tester, 1);
await tester.longPressAt(bPos, pointer: 7); await tester.longPressAt(bPos, pointer: 7);
await tester.pump(); await tester.pumpAndSettle();
final TextSelection selection = controller.selection; final TextSelection selection = controller.selection;
expect(selection.isCollapsed, false); expect(selection.isCollapsed, false);
expect(selection.baseOffset, 0); expect(selection.baseOffset, 0);
...@@ -11418,7 +11418,7 @@ void main() { ...@@ -11418,7 +11418,7 @@ void main() {
// Long press again keeps the selection menu visible. // Long press again keeps the selection menu visible.
await tester.longPressAt(textOffsetToPosition(tester, 0)); await tester.longPressAt(textOffsetToPosition(tester, 0));
await tester.pump(); await tester.pumpAndSettle();
expect(find.text('Paste'), findsOneWidget); expect(find.text('Paste'), findsOneWidget);
}, },
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu., skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.,
...@@ -11769,7 +11769,7 @@ void main() { ...@@ -11769,7 +11769,7 @@ void main() {
final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r' final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'
await tester.longPressAt(ePos); await tester.longPressAt(ePos);
await tester.pump(const Duration(milliseconds: 50)); await tester.pumpAndSettle(const Duration(milliseconds: 50));
// Tap slightly behind the previous tap to avoid tapping the context menu // Tap slightly behind the previous tap to avoid tapping the context menu
// on desktop. // on desktop.
...@@ -12737,7 +12737,7 @@ void main() { ...@@ -12737,7 +12737,7 @@ void main() {
final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater' final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'
await tester.longPressAt(wPos); await tester.longPressAt(wPos);
await tester.pump(const Duration(milliseconds: 50)); await tester.pumpAndSettle(const Duration(milliseconds: 50));
expect( expect(
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 3), const TextSelection.collapsed(offset: 3),
...@@ -17355,6 +17355,48 @@ void main() { ...@@ -17355,6 +17355,48 @@ void main() {
}, },
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
); );
testWidgets('Start the floating cursor on long tap', (WidgetTester tester) async {
EditableText.debugDeterministicCursor = true;
final TextEditingController controller = _textEditingController(
text: 'abcd',
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: RepaintBoundary(
key: const ValueKey<int>(1),
child: TextField(
autofocus: true,
controller: controller,
),
)
),
),
),
);
// Wait for autofocus.
await tester.pumpAndSettle();
final Offset textFieldCenter = tester.getCenter(find.byType(TextField));
final TestGesture gesture = await tester.startGesture(textFieldCenter);
await tester.pump(kLongPressTimeout);
await expectLater(
find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('text_field_floating_cursor.regular_and_floating_both.material.0.png'),
);
await gesture.moveTo(Offset(10, textFieldCenter.dy));
await tester.pump();
await expectLater(
find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('text_field_floating_cursor.only_floating_cursor.material.0.png'),
);
await gesture.up();
EditableText.debugDeterministicCursor = false;
},
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
);
} }
/// A Simple widget for testing the obscure text. /// A Simple widget for testing the obscure text.
......
...@@ -957,13 +957,6 @@ void main() { ...@@ -957,13 +957,6 @@ void main() {
await tester.pump(); await tester.pump();
expect(editable, paints expect(editable, paints
..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(463.3333435058594, -0.916666666666668, 465.3333435058594, 17.083333015441895),
const Radius.circular(2.0),
),
color: const Color(0xff999999),
)
..rrect( ..rrect(
rrect: RRect.fromRectAndRadius( rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(463.8333435058594, -0.916666666666668, 466.8333435058594, 19.083333969116211), const Rect.fromLTRB(463.8333435058594, -0.916666666666668, 466.8333435058594, 19.083333969116211),
...@@ -984,14 +977,32 @@ void main() { ...@@ -984,14 +977,32 @@ void main() {
expect(find.byType(EditableText), paints expect(find.byType(EditableText), paints
..rrect( ..rrect(
rrect: RRect.fromRectAndRadius( rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(191.3333282470703, -0.916666666666668, 193.3333282470703, 17.083333015441895), const Rect.fromLTRB(193.83334350585938, -0.916666666666668, 196.83334350585938, 19.083333969116211),
const Radius.circular(1.0),
),
color: const Color(0xbf2196f3),
),
);
// Move the cursor away from characters, this will show the regular cursor.
editableTextState.updateFloatingCursor(
RawFloatingCursorPoint(
state: FloatingCursorDragState.Update,
offset: const Offset(800, 0),
),
);
expect(find.byType(EditableText), paints
..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(719.3333333333333, -0.9166666666666679, 721.3333333333333, 17.083333333333332),
const Radius.circular(2.0), const Radius.circular(2.0),
), ),
color: const Color(0xff999999), color: const Color(0xff999999),
) )
..rrect( ..rrect(
rrect: RRect.fromRectAndRadius( rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(193.83334350585938, -0.916666666666668, 196.83334350585938, 19.083333969116211), const Rect.fromLTRB(800.5, -5.0, 803.5, 15.0),
const Radius.circular(1.0), const Radius.circular(1.0),
), ),
color: const Color(0xbf2196f3), color: const Color(0xbf2196f3),
...@@ -1302,4 +1313,88 @@ void main() { ...@@ -1302,4 +1313,88 @@ void main() {
}, },
variant: TargetPlatformVariant.all(), variant: TargetPlatformVariant.all(),
); );
testWidgets('Floating cursor showing with local position', (WidgetTester tester) async {
EditableText.debugDeterministicCursor = true;
final GlobalKey key = GlobalKey();
controller.text = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ\n1234567890';
controller.selection = const TextSelection.collapsed(offset: 0);
await tester.pumpWidget(
MaterialApp(
home: EditableText(
key: key,
autofocus: true,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
cursorOpacityAnimates: true,
maxLines: 2,
),
),
);
final EditableTextState state = tester.state(find.byType(EditableText));
state.updateFloatingCursor(
RawFloatingCursorPoint(
state: FloatingCursorDragState.Start,
offset: Offset.zero,
startLocation: (Offset.zero, TextPosition(offset: controller.selection.baseOffset, affinity: controller.selection.affinity))
)
);
await tester.pump();
expect(key.currentContext!.findRenderObject(), paints..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTWH(-0.5, -3.0, 3, 12),
const Radius.circular(1)
)
));
state.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update, offset: const Offset(51, 0)));
await tester.pump();
expect(key.currentContext!.findRenderObject(), paints..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTWH(50.5, -3.0, 3, 12),
const Radius.circular(1)
)
));
state.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End));
await tester.pumpAndSettle();
state.updateFloatingCursor(
RawFloatingCursorPoint(
state: FloatingCursorDragState.Start,
offset: Offset.zero,
startLocation: (const Offset(800, 10), TextPosition(offset: controller.selection.baseOffset, affinity: controller.selection.affinity))
)
);
await tester.pump();
expect(key.currentContext!.findRenderObject(), paints..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTWH(799.5, 4.0, 3, 12),
const Radius.circular(1)
)
));
state.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update, offset: const Offset(100, 10)));
await tester.pump();
expect(key.currentContext!.findRenderObject(), paints..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTWH(800.5, 14.0, 3, 12),
const Radius.circular(1)
)
));
state.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End));
await tester.pumpAndSettle();
EditableText.debugDeterministicCursor = false;
});
} }
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