mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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
This commit is contained in:
parent
83ac76050d
commit
0d4eb5eaa0
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user