Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit c4b8046

Browse files
authored
Floating cursor cleanup (#116746)
* Floating cursor cleanup * Use TextSelection.fromPosition
1 parent cbdc763 commit c4b8046

File tree

4 files changed

+237
-4
lines changed

4 files changed

+237
-4
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -3102,7 +3102,7 @@ class _FloatingCursorPainter extends RenderEditablePainter {
31023102
}
31033103

31043104
canvas.drawRRect(
3105-
RRect.fromRectAndRadius(floatingCursorRect.shift(renderEditable._paintOffset), _kFloatingCaretRadius),
3105+
RRect.fromRectAndRadius(floatingCursorRect, _kFloatingCaretRadius),
31063106
floatingCursorPaint..color = floatingCursorColor,
31073107
);
31083108
}

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

+5-3
Original file line numberDiff line numberDiff line change
@@ -2671,7 +2671,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
26712671
// we cache the position.
26722672
_pointOffsetOrigin = point.offset;
26732673

2674-
final TextPosition currentTextPosition = TextPosition(offset: renderEditable.selection!.baseOffset);
2674+
final TextPosition currentTextPosition = TextPosition(offset: renderEditable.selection!.baseOffset, affinity: renderEditable.selection!.affinity);
26752675
_startCaretRect = renderEditable.getLocalRectForCaret(currentTextPosition);
26762676

26772677
_lastBoundedOffset = _startCaretRect!.center - _floatingCursorOffset;
@@ -2702,9 +2702,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
27022702
final Offset finalPosition = renderEditable.getLocalRectForCaret(_lastTextPosition!).centerLeft - _floatingCursorOffset;
27032703
if (_floatingCursorResetController!.isCompleted) {
27042704
renderEditable.setFloatingCursor(FloatingCursorDragState.End, finalPosition, _lastTextPosition!);
2705-
if (_lastTextPosition!.offset != renderEditable.selection!.baseOffset) {
2705+
// Only change if new position is out of current selection range, as the
2706+
// selection may have been modified using the iOS keyboard selection gesture.
2707+
if (_lastTextPosition!.offset < renderEditable.selection!.start || _lastTextPosition!.offset >= renderEditable.selection!.end) {
27062708
// The cause is technically the force cursor, but the cause is listed as tap as the desired functionality is the same.
2707-
_handleSelectionChanged(TextSelection.collapsed(offset: _lastTextPosition!.offset), SelectionChangedCause.forcePress);
2709+
_handleSelectionChanged(TextSelection.fromPosition(_lastTextPosition!), SelectionChangedCause.forcePress);
27082710
}
27092711
_startCaretRect = null;
27102712
_lastTextPosition = null;

packages/flutter/test/rendering/editable_test.dart

+74
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'package:flutter/foundation.dart';
1212
import 'package:flutter/gestures.dart';
1313
import 'package:flutter/material.dart';
1414
import 'package:flutter/rendering.dart';
15+
import 'package:flutter/src/services/text_input.dart';
1516
import 'package:flutter_test/flutter_test.dart';
1617

1718
import 'mock_canvas.dart';
@@ -1725,6 +1726,79 @@ void main() {
17251726
editable.forceLine = false;
17261727
expect(editable.computeDryLayout(constraints).width, lessThan(initialWidth));
17271728
});
1729+
1730+
test('Floating cursor position is independent of viewport offset', () {
1731+
final TextSelectionDelegate delegate = _FakeEditableTextState();
1732+
final ValueNotifier<bool> showCursor = ValueNotifier<bool>(true);
1733+
EditableText.debugDeterministicCursor = true;
1734+
1735+
const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00);
1736+
1737+
final RenderEditable editable = RenderEditable(
1738+
backgroundCursorColor: Colors.grey,
1739+
textDirection: TextDirection.ltr,
1740+
cursorColor: cursorColor,
1741+
offset: ViewportOffset.zero(),
1742+
textSelectionDelegate: delegate,
1743+
text: const TextSpan(
1744+
text: 'test',
1745+
style: TextStyle(
1746+
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
1747+
),
1748+
),
1749+
maxLines: 3,
1750+
startHandleLayerLink: LayerLink(),
1751+
endHandleLayerLink: LayerLink(),
1752+
selection: const TextSelection.collapsed(
1753+
offset: 4,
1754+
affinity: TextAffinity.upstream,
1755+
),
1756+
);
1757+
1758+
layout(editable);
1759+
1760+
editable.layout(BoxConstraints.loose(const Size(100, 100)));
1761+
// Prepare for painting after layout.
1762+
pumpFrame(phase: EnginePhase.compositingBits);
1763+
1764+
expect(
1765+
editable,
1766+
// Draw no cursor by default.
1767+
paintsExactlyCountTimes(#drawRect, 0),
1768+
);
1769+
1770+
editable.showCursor = showCursor;
1771+
editable.setFloatingCursor(FloatingCursorDragState.Start, const Offset(50, 50), const TextPosition(
1772+
offset: 4,
1773+
affinity: TextAffinity.upstream,
1774+
));
1775+
pumpFrame(phase: EnginePhase.compositingBits);
1776+
1777+
final RRect expectedRRect = RRect.fromRectAndRadius(
1778+
const Rect.fromLTWH(49.5, 51, 2, 8),
1779+
const Radius.circular(1)
1780+
);
1781+
1782+
expect(editable, paints..rrect(
1783+
color: cursorColor.withOpacity(0.75),
1784+
rrect: expectedRRect
1785+
));
1786+
1787+
// Change the text viewport offset.
1788+
editable.offset = ViewportOffset.fixed(200);
1789+
1790+
// Floating cursor should be drawn in the same position.
1791+
editable.setFloatingCursor(FloatingCursorDragState.Start, const Offset(50, 50), const TextPosition(
1792+
offset: 4,
1793+
affinity: TextAffinity.upstream,
1794+
));
1795+
pumpFrame(phase: EnginePhase.compositingBits);
1796+
1797+
expect(editable, paints..rrect(
1798+
color: cursorColor.withOpacity(0.75),
1799+
rrect: expectedRRect
1800+
));
1801+
});
17281802
}
17291803

17301804
class _TestRenderEditable extends RenderEditable {

packages/flutter/test/widgets/editable_text_test.dart

+157
Original file line numberDiff line numberDiff line change
@@ -11701,6 +11701,163 @@ void main() {
1170111701
expect(tester.hasRunningAnimations, isFalse);
1170211702
});
1170311703

11704+
testWidgets('Floating cursor affinity', (WidgetTester tester) async {
11705+
EditableText.debugDeterministicCursor = true;
11706+
final FocusNode focusNode = FocusNode();
11707+
final GlobalKey key = GlobalKey();
11708+
// Set it up so that there will be word-wrap.
11709+
final TextEditingController controller = TextEditingController(text: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz');
11710+
await tester.pumpWidget(
11711+
MaterialApp(
11712+
home: Center(
11713+
child: ConstrainedBox(
11714+
constraints: const BoxConstraints(
11715+
maxWidth: 500,
11716+
),
11717+
child: EditableText(
11718+
key: key,
11719+
autofocus: true,
11720+
maxLines: 2,
11721+
controller: controller,
11722+
focusNode: focusNode,
11723+
style: textStyle,
11724+
cursorColor: Colors.blue,
11725+
backgroundCursorColor: Colors.grey,
11726+
cursorOpacityAnimates: true,
11727+
),
11728+
),
11729+
),
11730+
),
11731+
);
11732+
11733+
await tester.pump();
11734+
final EditableTextState state = tester.state(find.byType(EditableText));
11735+
11736+
// Select after the first word, with default affinity (downstream).
11737+
controller.selection = const TextSelection.collapsed(offset: 27);
11738+
await tester.pump();
11739+
state.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Start, offset: Offset.zero));
11740+
await tester.pump();
11741+
11742+
// The floating cursor should be drawn at the end of the first line.
11743+
expect(key.currentContext!.findRenderObject(), paints..rrect(
11744+
rrect: RRect.fromRectAndRadius(
11745+
const Rect.fromLTWH(0.5, 15, 3, 12),
11746+
const Radius.circular(1)
11747+
)
11748+
));
11749+
11750+
// Select after the first word, with upstream affinity.
11751+
controller.selection = const TextSelection.collapsed(offset: 27, affinity: TextAffinity.upstream);
11752+
await tester.pump();
11753+
11754+
state.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Start, offset: Offset.zero));
11755+
await tester.pump();
11756+
11757+
// The floating cursor should be drawn at the beginning of the second line.
11758+
expect(key.currentContext!.findRenderObject(), paints..rrect(
11759+
rrect: RRect.fromRectAndRadius(
11760+
const Rect.fromLTWH(378.5, 1, 3, 12),
11761+
const Radius.circular(1)
11762+
)
11763+
));
11764+
11765+
EditableText.debugDeterministicCursor = false;
11766+
});
11767+
11768+
testWidgets('Floating cursor ending with selection', (WidgetTester tester) async {
11769+
EditableText.debugDeterministicCursor = true;
11770+
final FocusNode focusNode = FocusNode();
11771+
final GlobalKey key = GlobalKey();
11772+
// Set it up so that there will be word-wrap.
11773+
final TextEditingController controller = TextEditingController(text: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
11774+
controller.selection = const TextSelection.collapsed(offset: 0);
11775+
await tester.pumpWidget(
11776+
MaterialApp(
11777+
home: EditableText(
11778+
key: key,
11779+
autofocus: true,
11780+
controller: controller,
11781+
focusNode: focusNode,
11782+
style: textStyle,
11783+
cursorColor: Colors.blue,
11784+
backgroundCursorColor: Colors.grey,
11785+
cursorOpacityAnimates: true,
11786+
),
11787+
),
11788+
);
11789+
11790+
await tester.pump();
11791+
final EditableTextState state = tester.state(find.byType(EditableText));
11792+
11793+
state.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Start, offset: Offset.zero));
11794+
await tester.pump();
11795+
11796+
// The floating cursor should be drawn at the start of the line.
11797+
expect(key.currentContext!.findRenderObject(), paints..rrect(
11798+
rrect: RRect.fromRectAndRadius(
11799+
const Rect.fromLTWH(0.5, 1, 3, 12),
11800+
const Radius.circular(1)
11801+
)
11802+
));
11803+
11804+
state.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update, offset: const Offset(50, 0)));
11805+
await tester.pump();
11806+
11807+
// The floating cursor should be drawn somewhere in the middle of the line
11808+
expect(key.currentContext!.findRenderObject(), paints..rrect(
11809+
rrect: RRect.fromRectAndRadius(
11810+
const Rect.fromLTWH(50.5, 1, 3, 12),
11811+
const Radius.circular(1)
11812+
)
11813+
));
11814+
11815+
state.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End, offset: Offset.zero));
11816+
await tester.pumpAndSettle(const Duration(milliseconds: 125)); // Floating cursor has an end animation.
11817+
11818+
// Selection should be updated based on the floating cursor location.
11819+
expect(controller.selection.isCollapsed, true);
11820+
expect(controller.selection.baseOffset, 4);
11821+
11822+
state.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Start, offset: Offset.zero));
11823+
await tester.pump();
11824+
11825+
// The floating cursor should be drawn near to the previous position.
11826+
// It's different because it's snapped to exactly between characters.
11827+
expect(key.currentContext!.findRenderObject(), paints..rrect(
11828+
rrect: RRect.fromRectAndRadius(
11829+
const Rect.fromLTWH(56.5, 1, 3, 12),
11830+
const Radius.circular(1)
11831+
)
11832+
));
11833+
11834+
state.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update, offset: const Offset(-56, 0)));
11835+
await tester.pump();
11836+
11837+
// The floating cursor should be drawn at the start of the line.
11838+
expect(key.currentContext!.findRenderObject(), paints..rrect(
11839+
rrect: RRect.fromRectAndRadius(
11840+
const Rect.fromLTWH(0.5, 1, 3, 12),
11841+
const Radius.circular(1)
11842+
)
11843+
));
11844+
11845+
// Simulate UIKit setting the selection using keyboard selection.
11846+
controller.selection = const TextSelection(baseOffset: 0, extentOffset: 4);
11847+
await tester.pump();
11848+
11849+
state.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End, offset: Offset.zero));
11850+
await tester.pump();
11851+
11852+
// Selection should not be updated as the new position is within the selection range.
11853+
expect(controller.selection.isCollapsed, false);
11854+
expect(controller.selection.baseOffset, 0);
11855+
expect(controller.selection.extentOffset, 4);
11856+
11857+
EditableText.debugDeterministicCursor = false;
11858+
});
11859+
11860+
1170411861
group('Selection changed scroll into view', () {
1170511862
final String text = List<int>.generate(64, (int index) => index).join('\n');
1170611863
final TextEditingController controller = TextEditingController(text: text);

0 commit comments

Comments
 (0)