Skip to content

Commit 0d4eb5e

Browse files
authored
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
1 parent 83ac760 commit 0d4eb5e

File tree

7 files changed

+311
-63
lines changed

7 files changed

+311
-63
lines changed

packages/flutter/lib/src/rendering/editable.dart

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ const EdgeInsets _kFloatingCursorSizeIncrease = EdgeInsets.symmetric(horizontal:
3030
// The corner radius of the floating cursor in pixels.
3131
const Radius _kFloatingCursorRadius = Radius.circular(1.0);
3232

33+
// This constant represents the shortest squared distance required between the floating cursor
34+
// and the regular cursor when both are present in the text field.
35+
// If the squared distance between the two cursors is less than this value,
36+
// it's not necessary to display both cursors at the same time.
37+
// This behavior is consistent with the one observed in iOS UITextField.
38+
const double _kShortestDistanceSquaredWithFloatingAndRegularCursors = 15.0 * 15.0;
39+
3340
/// Represents the coordinates of the point in a selection, and the text
3441
/// direction at that point, relative to top left of the [RenderEditable] that
3542
/// holds the selection.
@@ -2360,19 +2367,35 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
23602367
// difference in the rendering position and the raw offset value.
23612368
Offset _relativeOrigin = Offset.zero;
23622369
Offset? _previousOffset;
2370+
bool _shouldResetOrigin = true;
23632371
bool _resetOriginOnLeft = false;
23642372
bool _resetOriginOnRight = false;
23652373
bool _resetOriginOnTop = false;
23662374
bool _resetOriginOnBottom = false;
23672375
double? _resetFloatingCursorAnimationValue;
23682376

2377+
static Offset _calculateAdjustedCursorOffset(Offset offset, Rect boundingRects) {
2378+
final double adjustedX = clampDouble(offset.dx, boundingRects.left, boundingRects.right);
2379+
final double adjustedY = clampDouble(offset.dy, boundingRects.top, boundingRects.bottom);
2380+
return Offset(adjustedX, adjustedY);
2381+
}
2382+
23692383
/// Returns the position within the text field closest to the raw cursor offset.
2370-
Offset calculateBoundedFloatingCursorOffset(Offset rawCursorOffset) {
2384+
Offset calculateBoundedFloatingCursorOffset(Offset rawCursorOffset, {bool? shouldResetOrigin}) {
23712385
Offset deltaPosition = Offset.zero;
23722386
final double topBound = -floatingCursorAddedMargin.top;
2373-
final double bottomBound = _textPainter.height - preferredLineHeight + floatingCursorAddedMargin.bottom;
2387+
final double bottomBound = math.min(size.height, _textPainter.height) - preferredLineHeight + floatingCursorAddedMargin.bottom;
23742388
final double leftBound = -floatingCursorAddedMargin.left;
2375-
final double rightBound = _textPainter.width + floatingCursorAddedMargin.right;
2389+
final double rightBound = math.min(size.width, _textPainter.width) + floatingCursorAddedMargin.right;
2390+
final Rect boundingRects = Rect.fromLTRB(leftBound, topBound, rightBound, bottomBound);
2391+
2392+
if (shouldResetOrigin != null) {
2393+
_shouldResetOrigin = shouldResetOrigin;
2394+
}
2395+
2396+
if (!_shouldResetOrigin) {
2397+
return _calculateAdjustedCursorOffset(rawCursorOffset, boundingRects);
2398+
}
23762399

23772400
if (_previousOffset != null) {
23782401
deltaPosition = rawCursorOffset - _previousOffset!;
@@ -2381,34 +2404,32 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
23812404
// If the raw cursor offset has gone off an edge, we want to reset the relative
23822405
// origin of the dragging when the user drags back into the field.
23832406
if (_resetOriginOnLeft && deltaPosition.dx > 0) {
2384-
_relativeOrigin = Offset(rawCursorOffset.dx - leftBound, _relativeOrigin.dy);
2407+
_relativeOrigin = Offset(rawCursorOffset.dx - boundingRects.left, _relativeOrigin.dy);
23852408
_resetOriginOnLeft = false;
23862409
} else if (_resetOriginOnRight && deltaPosition.dx < 0) {
2387-
_relativeOrigin = Offset(rawCursorOffset.dx - rightBound, _relativeOrigin.dy);
2410+
_relativeOrigin = Offset(rawCursorOffset.dx - boundingRects.right, _relativeOrigin.dy);
23882411
_resetOriginOnRight = false;
23892412
}
23902413
if (_resetOriginOnTop && deltaPosition.dy > 0) {
2391-
_relativeOrigin = Offset(_relativeOrigin.dx, rawCursorOffset.dy - topBound);
2414+
_relativeOrigin = Offset(_relativeOrigin.dx, rawCursorOffset.dy - boundingRects.top);
23922415
_resetOriginOnTop = false;
23932416
} else if (_resetOriginOnBottom && deltaPosition.dy < 0) {
2394-
_relativeOrigin = Offset(_relativeOrigin.dx, rawCursorOffset.dy - bottomBound);
2417+
_relativeOrigin = Offset(_relativeOrigin.dx, rawCursorOffset.dy - boundingRects.bottom);
23952418
_resetOriginOnBottom = false;
23962419
}
23972420

23982421
final double currentX = rawCursorOffset.dx - _relativeOrigin.dx;
23992422
final double currentY = rawCursorOffset.dy - _relativeOrigin.dy;
2400-
final double adjustedX = math.min(math.max(currentX, leftBound), rightBound);
2401-
final double adjustedY = math.min(math.max(currentY, topBound), bottomBound);
2402-
final Offset adjustedOffset = Offset(adjustedX, adjustedY);
2423+
final Offset adjustedOffset = _calculateAdjustedCursorOffset(Offset(currentX, currentY), boundingRects);
24032424

2404-
if (currentX < leftBound && deltaPosition.dx < 0) {
2425+
if (currentX < boundingRects.left && deltaPosition.dx < 0) {
24052426
_resetOriginOnLeft = true;
2406-
} else if (currentX > rightBound && deltaPosition.dx > 0) {
2427+
} else if (currentX > boundingRects.right && deltaPosition.dx > 0) {
24072428
_resetOriginOnRight = true;
24082429
}
2409-
if (currentY < topBound && deltaPosition.dy < 0) {
2430+
if (currentY < boundingRects.top && deltaPosition.dy < 0) {
24102431
_resetOriginOnTop = true;
2411-
} else if (currentY > bottomBound && deltaPosition.dy > 0) {
2432+
} else if (currentY > boundingRects.bottom && deltaPosition.dy > 0) {
24122433
_resetOriginOnBottom = true;
24132434
}
24142435

@@ -2420,9 +2441,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
24202441
/// Sets the screen position of the floating cursor and the text position
24212442
/// closest to the cursor.
24222443
void setFloatingCursor(FloatingCursorDragState state, Offset boundedOffset, TextPosition lastTextPosition, { double? resetLerpValue }) {
2423-
if (state == FloatingCursorDragState.Start) {
2444+
if (state == FloatingCursorDragState.End) {
24242445
_relativeOrigin = Offset.zero;
24252446
_previousOffset = null;
2447+
_shouldResetOrigin = true;
24262448
_resetOriginOnBottom = false;
24272449
_resetOriginOnTop = false;
24282450
_resetOriginOnRight = false;
@@ -2898,6 +2920,12 @@ class _CaretPainter extends RenderEditablePainter {
28982920
void paintRegularCursor(Canvas canvas, RenderEditable renderEditable, Color caretColor, TextPosition textPosition) {
28992921
final Rect integralRect = renderEditable.getLocalRectForCaret(textPosition);
29002922
if (shouldPaint) {
2923+
if (floatingCursorRect != null) {
2924+
final double distanceSquared = (floatingCursorRect!.center - integralRect.center).distanceSquared;
2925+
if (distanceSquared < _kShortestDistanceSquaredWithFloatingAndRegularCursors) {
2926+
return;
2927+
}
2928+
}
29012929
final Radius? radius = cursorRadius;
29022930
caretPaint.color = caretColor;
29032931
if (radius == null) {

packages/flutter/lib/src/services/text_input.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,12 +744,18 @@ class RawFloatingCursorPoint {
744744
/// [FloatingCursorDragState.Update].
745745
RawFloatingCursorPoint({
746746
this.offset,
747+
this.startLocation,
747748
required this.state,
748749
}) : assert(state != FloatingCursorDragState.Update || offset != null);
749750

750751
/// The raw position of the floating cursor as determined by the iOS sdk.
751752
final Offset? offset;
752753

754+
/// Represents the starting location when initiating a floating cursor via long press.
755+
/// This is a tuple where the first item is the local offset and the second item is the new caret position.
756+
/// This is only non-null when a floating cursor is started.
757+
final (Offset, TextPosition)? startLocation;
758+
753759
/// The state of the floating cursor.
754760
final FloatingCursorDragState state;
755761
}

packages/flutter/lib/src/widgets/editable_text.dart

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3162,7 +3162,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
31623162
}
31633163

31643164
// The original position of the caret on FloatingCursorDragState.start.
3165-
Rect? _startCaretRect;
3165+
Offset? _startCaretCenter;
31663166

31673167
// The most recent text position as determined by the location of the floating
31683168
// cursor.
@@ -3197,15 +3197,26 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
31973197
// we cache the position.
31983198
_pointOffsetOrigin = point.offset;
31993199

3200-
final TextPosition currentTextPosition = TextPosition(offset: renderEditable.selection!.baseOffset, affinity: renderEditable.selection!.affinity);
3201-
_startCaretRect = renderEditable.getLocalRectForCaret(currentTextPosition);
3200+
final Offset startCaretCenter;
3201+
final TextPosition currentTextPosition;
3202+
final bool shouldResetOrigin;
3203+
// Only non-null when starting a floating cursor via long press.
3204+
if (point.startLocation != null) {
3205+
shouldResetOrigin = false;
3206+
(startCaretCenter, currentTextPosition) = point.startLocation!;
3207+
} else {
3208+
shouldResetOrigin = true;
3209+
currentTextPosition = TextPosition(offset: renderEditable.selection!.baseOffset, affinity: renderEditable.selection!.affinity);
3210+
startCaretCenter = renderEditable.getLocalRectForCaret(currentTextPosition).center;
3211+
}
32023212

3203-
_lastBoundedOffset = _startCaretRect!.center - _floatingCursorOffset;
3213+
_startCaretCenter = startCaretCenter;
3214+
_lastBoundedOffset = renderEditable.calculateBoundedFloatingCursorOffset(_startCaretCenter! - _floatingCursorOffset, shouldResetOrigin: shouldResetOrigin);
32043215
_lastTextPosition = currentTextPosition;
32053216
renderEditable.setFloatingCursor(point.state, _lastBoundedOffset!, _lastTextPosition!);
32063217
case FloatingCursorDragState.Update:
32073218
final Offset centeredPoint = point.offset! - _pointOffsetOrigin!;
3208-
final Offset rawCursorOffset = _startCaretRect!.center + centeredPoint - _floatingCursorOffset;
3219+
final Offset rawCursorOffset = _startCaretCenter! + centeredPoint - _floatingCursorOffset;
32093220

32103221
_lastBoundedOffset = renderEditable.calculateBoundedFloatingCursorOffset(rawCursorOffset);
32113222
_lastTextPosition = renderEditable.getPositionForPoint(renderEditable.localToGlobal(_lastBoundedOffset! + _floatingCursorOffset));
@@ -3245,7 +3256,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
32453256
// The cause is technically the force cursor, but the cause is listed as tap as the desired functionality is the same.
32463257
_handleSelectionChanged(TextSelection.fromPosition(_lastTextPosition!), SelectionChangedCause.forcePress);
32473258
}
3248-
_startCaretRect = null;
3259+
_startCaretCenter = null;
32493260
_lastTextPosition = null;
32503261
_pointOffsetOrigin = null;
32513262
_lastBoundedOffset = null;

packages/flutter/lib/src/widgets/text_selection.dart

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2453,6 +2453,19 @@ class TextSelectionGestureDetectorBuilder {
24532453
from: details.globalPosition,
24542454
cause: SelectionChangedCause.longPress,
24552455
);
2456+
// Show the floating cursor.
2457+
final RawFloatingCursorPoint cursorPoint = RawFloatingCursorPoint(
2458+
state: FloatingCursorDragState.Start,
2459+
startLocation: (
2460+
renderEditable.globalToLocal(details.globalPosition),
2461+
TextPosition(
2462+
offset: editableText.textEditingValue.selection.baseOffset,
2463+
affinity: editableText.textEditingValue.selection.affinity,
2464+
),
2465+
),
2466+
offset: Offset.zero,
2467+
);
2468+
editableText.updateFloatingCursor(cursorPoint);
24562469
}
24572470
case TargetPlatform.android:
24582471
case TargetPlatform.fuchsia:
@@ -2488,7 +2501,6 @@ class TextSelectionGestureDetectorBuilder {
24882501
0.0,
24892502
_scrollPosition - _dragStartScrollOffset,
24902503
);
2491-
24922504
switch (defaultTargetPlatform) {
24932505
case TargetPlatform.iOS:
24942506
case TargetPlatform.macOS:
@@ -2503,6 +2515,12 @@ class TextSelectionGestureDetectorBuilder {
25032515
from: details.globalPosition,
25042516
cause: SelectionChangedCause.longPress,
25052517
);
2518+
// Update the floating cursor.
2519+
final RawFloatingCursorPoint cursorPoint = RawFloatingCursorPoint(
2520+
state: FloatingCursorDragState.Update,
2521+
offset: details.offsetFromOrigin,
2522+
);
2523+
editableText.updateFloatingCursor(cursorPoint);
25062524
}
25072525
case TargetPlatform.android:
25082526
case TargetPlatform.fuchsia:
@@ -2536,6 +2554,13 @@ class TextSelectionGestureDetectorBuilder {
25362554
_longPressStartedWithoutFocus = false;
25372555
_dragStartViewportOffset = 0.0;
25382556
_dragStartScrollOffset = 0.0;
2557+
if (defaultTargetPlatform == TargetPlatform.iOS && delegate.selectionEnabled && editableText.textEditingValue.selection.isCollapsed) {
2558+
// Update the floating cursor.
2559+
final RawFloatingCursorPoint cursorPoint = RawFloatingCursorPoint(
2560+
state: FloatingCursorDragState.End
2561+
);
2562+
editableText.updateFloatingCursor(cursorPoint);
2563+
}
25392564
}
25402565

25412566
/// Handler for [TextSelectionGestureDetector.onSecondaryTap].

0 commit comments

Comments
 (0)