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:
// The corner radius of the floating cursor in pixels.
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
/// direction at that point, relative to top left of the [RenderEditable] that
/// holds the selection.
......@@ -2360,19 +2367,35 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
// difference in the rendering position and the raw offset value.
Offset _relativeOrigin = Offset.zero;
Offset? _previousOffset;
bool _shouldResetOrigin = true;
bool _resetOriginOnLeft = false;
bool _resetOriginOnRight = false;
bool _resetOriginOnTop = false;
bool _resetOriginOnBottom = false;
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.
Offset calculateBoundedFloatingCursorOffset(Offset rawCursorOffset) {
Offset calculateBoundedFloatingCursorOffset(Offset rawCursorOffset, {bool? shouldResetOrigin}) {
Offset deltaPosition = Offset.zero;
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 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) {
deltaPosition = rawCursorOffset - _previousOffset!;
......@@ -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
// origin of the dragging when the user drags back into the field.
if (_resetOriginOnLeft && deltaPosition.dx > 0) {
_relativeOrigin = Offset(rawCursorOffset.dx - leftBound, _relativeOrigin.dy);
_relativeOrigin = Offset(rawCursorOffset.dx - boundingRects.left, _relativeOrigin.dy);
_resetOriginOnLeft = false;
} else if (_resetOriginOnRight && deltaPosition.dx < 0) {
_relativeOrigin = Offset(rawCursorOffset.dx - rightBound, _relativeOrigin.dy);
_relativeOrigin = Offset(rawCursorOffset.dx - boundingRects.right, _relativeOrigin.dy);
_resetOriginOnRight = false;
}
if (_resetOriginOnTop && deltaPosition.dy > 0) {
_relativeOrigin = Offset(_relativeOrigin.dx, rawCursorOffset.dy - topBound);
_relativeOrigin = Offset(_relativeOrigin.dx, rawCursorOffset.dy - boundingRects.top);
_resetOriginOnTop = false;
} else if (_resetOriginOnBottom && deltaPosition.dy < 0) {
_relativeOrigin = Offset(_relativeOrigin.dx, rawCursorOffset.dy - bottomBound);
_relativeOrigin = Offset(_relativeOrigin.dx, rawCursorOffset.dy - boundingRects.bottom);
_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);
final Offset adjustedOffset = _calculateAdjustedCursorOffset(Offset(currentX, currentY), boundingRects);
if (currentX < leftBound && deltaPosition.dx < 0) {
if (currentX < boundingRects.left && deltaPosition.dx < 0) {
_resetOriginOnLeft = true;
} else if (currentX > rightBound && deltaPosition.dx > 0) {
} else if (currentX > boundingRects.right && deltaPosition.dx > 0) {
_resetOriginOnRight = true;
}
if (currentY < topBound && deltaPosition.dy < 0) {
if (currentY < boundingRects.top && deltaPosition.dy < 0) {
_resetOriginOnTop = true;
} else if (currentY > bottomBound && deltaPosition.dy > 0) {
} else if (currentY > boundingRects.bottom && deltaPosition.dy > 0) {
_resetOriginOnBottom = true;
}
......@@ -2420,9 +2441,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
/// 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 }) {
if (state == FloatingCursorDragState.Start) {
if (state == FloatingCursorDragState.End) {
_relativeOrigin = Offset.zero;
_previousOffset = null;
_shouldResetOrigin = true;
_resetOriginOnBottom = false;
_resetOriginOnTop = false;
_resetOriginOnRight = false;
......@@ -2898,6 +2920,12 @@ class _CaretPainter extends RenderEditablePainter {
void paintRegularCursor(Canvas canvas, RenderEditable renderEditable, Color caretColor, TextPosition textPosition) {
final Rect integralRect = renderEditable.getLocalRectForCaret(textPosition);
if (shouldPaint) {
if (floatingCursorRect != null) {
final double distanceSquared = (floatingCursorRect!.center - integralRect.center).distanceSquared;
if (distanceSquared < _kShortestDistanceSquaredWithFloatingAndRegularCursors) {
return;
}
}
final Radius? radius = cursorRadius;
caretPaint.color = caretColor;
if (radius == null) {
......
......@@ -744,12 +744,18 @@ class RawFloatingCursorPoint {
/// [FloatingCursorDragState.Update].
RawFloatingCursorPoint({
this.offset,
this.startLocation,
required this.state,
}) : assert(state != FloatingCursorDragState.Update || offset != null);
/// The raw position of the floating cursor as determined by the iOS sdk.
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.
final FloatingCursorDragState state;
}
......
......@@ -3162,7 +3162,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
// 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
// cursor.
......@@ -3197,15 +3197,26 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// we cache the position.
_pointOffsetOrigin = point.offset;
final TextPosition currentTextPosition = TextPosition(offset: renderEditable.selection!.baseOffset, affinity: renderEditable.selection!.affinity);
_startCaretRect = renderEditable.getLocalRectForCaret(currentTextPosition);
final Offset startCaretCenter;
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;
renderEditable.setFloatingCursor(point.state, _lastBoundedOffset!, _lastTextPosition!);
case FloatingCursorDragState.Update:
final Offset centeredPoint = point.offset! - _pointOffsetOrigin!;
final Offset rawCursorOffset = _startCaretRect!.center + centeredPoint - _floatingCursorOffset;
final Offset rawCursorOffset = _startCaretCenter! + centeredPoint - _floatingCursorOffset;
_lastBoundedOffset = renderEditable.calculateBoundedFloatingCursorOffset(rawCursorOffset);
_lastTextPosition = renderEditable.getPositionForPoint(renderEditable.localToGlobal(_lastBoundedOffset! + _floatingCursorOffset));
......@@ -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.
_handleSelectionChanged(TextSelection.fromPosition(_lastTextPosition!), SelectionChangedCause.forcePress);
}
_startCaretRect = null;
_startCaretCenter = null;
_lastTextPosition = null;
_pointOffsetOrigin = null;
_lastBoundedOffset = null;
......
......@@ -2453,6 +2453,19 @@ class TextSelectionGestureDetectorBuilder {
from: details.globalPosition,
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.fuchsia:
......@@ -2488,7 +2501,6 @@ class TextSelectionGestureDetectorBuilder {
0.0,
_scrollPosition - _dragStartScrollOffset,
);
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
......@@ -2503,6 +2515,12 @@ class TextSelectionGestureDetectorBuilder {
from: details.globalPosition,
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.fuchsia:
......@@ -2536,6 +2554,13 @@ class TextSelectionGestureDetectorBuilder {
_longPressStartedWithoutFocus = false;
_dragStartViewportOffset = 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].
......
......@@ -12,7 +12,7 @@ import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle, Color;
import 'package:flutter/cupertino.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/rendering.dart';
import 'package:flutter/services.dart';
......@@ -279,7 +279,7 @@ void main() {
// Long press to put the cursor after the "s".
const int index = 3;
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.
await tester.tapAt(textOffsetToPosition(tester, index));
......@@ -329,7 +329,7 @@ void main() {
// Long press to put the cursor after the "s".
const int index = 3;
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.
await tester.tapAt(textOffsetToPosition(tester, index));
......@@ -377,7 +377,7 @@ void main() {
// Long press to put the cursor after the "s".
const int index = 3;
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.
await tester.tapAt(textOffsetToPosition(tester, index));
......@@ -1749,7 +1749,7 @@ void main() {
await tester.longPressAt(
tester.getTopRight(find.text("j'aime la poutine")),
);
await tester.pump();
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 200));
Text text = tester.widget<Text>(find.text('Paste'));
......@@ -1780,7 +1780,7 @@ void main() {
await tester.longPressAt(
tester.getTopRight(find.text("j'aime la poutine")),
);
await tester.pump();
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 200));
text = tester.widget<Text>(find.text('Paste'));
......@@ -1816,7 +1816,7 @@ void main() {
// Long press to put the cursor after the "w".
const int index = 3;
await tester.longPressAt(textOffsetToPosition(tester, index));
await tester.pump();
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: index),
......@@ -1863,7 +1863,7 @@ void main() {
// Long press to select 'Atwater'
const int index = 3;
await tester.longPressAt(textOffsetToPosition(tester, index));
await tester.pump();
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
......@@ -1922,7 +1922,7 @@ void main() {
tester.getTopRight(find.text('readonly')),
);
await tester.pump();
await tester.pumpAndSettle();
expect(find.text('Paste'), findsNothing);
expect(find.text('Cut'), findsNothing);
......@@ -1962,7 +1962,7 @@ void main() {
await tester.longPressAt(
tester.getTopRight(find.text("j'aime la poutine")),
);
await tester.pump();
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 200));
await tester.tap(find.text('Select All'));
......@@ -2244,7 +2244,7 @@ void main() {
// Long press to select 'Atwater'.
const int index = 3;
await tester.longPressAt(textOffsetToPosition(tester, index));
await tester.pump();
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
......@@ -2304,7 +2304,7 @@ void main() {
// Long press to put the cursor after the "w".
const int index = 3;
await tester.longPressAt(textOffsetToPosition(tester, index));
await tester.pump();
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: index),
......@@ -2349,7 +2349,7 @@ void main() {
// Long press to put the cursor after the "w".
const int index = 3;
await tester.longPressAt(textOffsetToPosition(tester, index));
await tester.pump();
await tester.pumpAndSettle();
// Second tap doesn't select anything.
await tester.tapAt(textOffsetToPosition(tester, index));
......@@ -2993,7 +2993,7 @@ void main() {
await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
await tester.pump(const Duration(milliseconds: 50));
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.
expect(find.text('Paste'), findsOneWidget);
......@@ -3007,7 +3007,7 @@ void main() {
await tester.pump(const Duration(milliseconds: 50));
// Long tap at the end.
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.
expect(find.text('Paste'), findsOneWidget);
......@@ -3110,7 +3110,7 @@ void main() {
final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'
await tester.longPressAt(ePos);
await tester.pump(const Duration(milliseconds: 50));
await tester.pumpAndSettle(const Duration(milliseconds: 50));
expectCupertinoToolbarForCollapsedSelection();
......@@ -3524,7 +3524,7 @@ void main() {
final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'
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.baseOffset, 3);
......@@ -7986,7 +7986,7 @@ void main() {
final Offset textFieldStart = tester.getTopLeft(find.byKey(const Key('field0')));
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.
await tester.tapAt(textFieldStart + const Offset(20.0, 100.0));
await tester.pump(const Duration(milliseconds: 300));
......@@ -8040,7 +8040,7 @@ void main() {
final Offset textFieldStart = tester.getTopLeft(find.byKey(const Key('field0')));
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.
await tester.tapAt(textFieldStart + const Offset(20.0, 100.0));
await tester.pump(const Duration(milliseconds: 300));
......@@ -10094,4 +10094,45 @@ void main() {
// placeholder.
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() {
// Long press to put the cursor after the "s".
const int index = 3;
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.
await tester.tapAt(textOffsetToPosition(tester, index));
......@@ -561,7 +561,7 @@ void main() {
// Long press to put the cursor after the "s".
const int index = 3;
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.
await tester.tapAt(textOffsetToPosition(tester, index));
......@@ -610,7 +610,7 @@ void main() {
// Long press to put the cursor after the "s".
const int index = 3;
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.
await tester.tapAt(textOffsetToPosition(tester, index));
......@@ -1244,7 +1244,7 @@ void main() {
final Offset textfieldStart = tester.getTopLeft(find.byKey(const Key('field0')));
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.pump(const Duration(milliseconds: 300));
......@@ -1715,7 +1715,7 @@ void main() {
// Long press the 'e' to select 'def'.
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
await tester.longPressAt(ePos, pointer: 7);
await tester.pump();
await tester.pumpAndSettle();
// 'def' is selected.
expect(controller.selection.baseOffset, testValue.indexOf('d'));
......@@ -2174,7 +2174,7 @@ void main() {
// Long press the 'e' to select 'def'.
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
await tester.longPressAt(ePos, pointer: 7);
await tester.pump();
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.length);
......@@ -4209,7 +4209,7 @@ void main() {
// Long press does select text.
final Offset ePos = textOffsetToPosition(tester, 1);
await tester.longPressAt(ePos, pointer: 7);
await tester.pump();
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, false);
});
......@@ -4237,7 +4237,7 @@ void main() {
// Long press doesn't select text.
final Offset ePos2 = textOffsetToPosition(tester, 1);
await tester.longPressAt(ePos2, pointer: 7);
await tester.pump();
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
});
......@@ -4265,7 +4265,7 @@ void main() {
// Long press doesn't select text.
final Offset ePos2 = textOffsetToPosition(tester, 1);
await tester.longPressAt(ePos2, pointer: 7);
await tester.pump();
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
});
......@@ -4284,7 +4284,7 @@ void main() {
// Long press does select text.
final Offset bPos = textOffsetToPosition(tester, 1);
await tester.longPressAt(bPos, pointer: 7);
await tester.pump();
await tester.pumpAndSettle();
final TextSelection selection = controller.selection;
expect(selection.isCollapsed, false);
expect(selection.baseOffset, 0);
......@@ -11418,7 +11418,7 @@ void main() {
// Long press again keeps the selection menu visible.
await tester.longPressAt(textOffsetToPosition(tester, 0));
await tester.pump();
await tester.pumpAndSettle();
expect(find.text('Paste'), findsOneWidget);
},
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.,
......@@ -11769,7 +11769,7 @@ void main() {
final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'
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
// on desktop.
......@@ -12737,7 +12737,7 @@ void main() {
final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'
await tester.longPressAt(wPos);
await tester.pump(const Duration(milliseconds: 50));
await tester.pumpAndSettle(const Duration(milliseconds: 50));
expect(
controller.selection,
const TextSelection.collapsed(offset: 3),
......@@ -17355,6 +17355,48 @@ void main() {
},
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.
......
......@@ -957,13 +957,6 @@ void main() {
await tester.pump();
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.fromRectAndRadius(
const Rect.fromLTRB(463.8333435058594, -0.916666666666668, 466.8333435058594, 19.083333969116211),
......@@ -984,14 +977,32 @@ void main() {
expect(find.byType(EditableText), paints
..rrect(
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),
),
color: const Color(0xff999999),
)
..rrect(
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),
),
color: const Color(0xbf2196f3),
......@@ -1302,4 +1313,88 @@ void main() {
},
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