Skip to content

Commit 7675a6e

Browse files
authored
Add support for text selection via mouse to Cupertino text fields (flutter#29769)
1 parent bfa1d25 commit 7675a6e

File tree

3 files changed

+123
-5
lines changed

3 files changed

+123
-5
lines changed

packages/flutter/lib/src/cupertino/text_field.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,28 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
533533
_editableTextKey.currentState.showToolbar();
534534
}
535535

536+
void _handleMouseDragSelectionStart(DragStartDetails details) {
537+
_renderEditable.selectPositionAt(
538+
from: details.globalPosition,
539+
cause: SelectionChangedCause.drag,
540+
);
541+
}
542+
543+
void _handleMouseDragSelectionUpdate(
544+
DragStartDetails startDetails,
545+
DragUpdateDetails updateDetails,
546+
) {
547+
_renderEditable.selectPositionAt(
548+
from: startDetails.globalPosition,
549+
to: updateDetails.globalPosition,
550+
cause: SelectionChangedCause.drag,
551+
);
552+
}
553+
554+
void _handleMouseDragSelectionEnd(DragEndDetails details) {
555+
_requestKeyboard();
556+
}
557+
536558
void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) {
537559
if (cause == SelectionChangedCause.longPress) {
538560
_editableTextKey.currentState?.bringIntoView(selection.base);
@@ -742,6 +764,9 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
742764
onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate,
743765
onSingleLongTapEnd: _handleSingleLongTapEnd,
744766
onDoubleTapDown: _handleDoubleTapDown,
767+
onDragSelectionStart: _handleMouseDragSelectionStart,
768+
onDragSelectionUpdate: _handleMouseDragSelectionUpdate,
769+
onDragSelectionEnd: _handleMouseDragSelectionEnd,
745770
behavior: HitTestBehavior.translucent,
746771
child: _addTextDependentAttachments(paddedEditable, textStyle),
747772
),

packages/flutter/lib/src/material/text_field.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -743,15 +743,15 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
743743
}
744744
}
745745

746-
void _handleDragSelectionStart(DragStartDetails details) {
746+
void _handleMouseDragSelectionStart(DragStartDetails details) {
747747
_renderEditable.selectPositionAt(
748748
from: details.globalPosition,
749749
cause: SelectionChangedCause.drag,
750750
);
751751
_startSplash(details.globalPosition);
752752
}
753753

754-
void _handleDragSelectionUpdate(
754+
void _handleMouseDragSelectionUpdate(
755755
DragStartDetails startDetails,
756756
DragUpdateDetails updateDetails,
757757
) {
@@ -930,8 +930,8 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
930930
onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate,
931931
onSingleLongTapEnd: _handleSingleLongTapEnd,
932932
onDoubleTapDown: _handleDoubleTapDown,
933-
onDragSelectionStart: _handleDragSelectionStart,
934-
onDragSelectionUpdate: _handleDragSelectionUpdate,
933+
onDragSelectionStart: _handleMouseDragSelectionStart,
934+
onDragSelectionUpdate: _handleMouseDragSelectionUpdate,
935935
behavior: HitTestBehavior.translucent,
936936
child: child,
937937
),

packages/flutter/test/cupertino/text_field_test.dart

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import 'package:flutter/cupertino.dart';
88
import 'package:flutter/rendering.dart';
99
import 'package:flutter/services.dart';
1010
import 'package:flutter/foundation.dart';
11-
import 'package:flutter/gestures.dart' show DragStartBehavior;
11+
import 'package:flutter/gestures.dart' show DragStartBehavior, PointerDeviceKind;
1212
import 'package:flutter_test/flutter_test.dart';
1313

1414
class MockClipboard {
@@ -1791,6 +1791,99 @@ void main() {
17911791
expect(controller.selection.extentOffset, 5);
17921792
});
17931793

1794+
testWidgets('Can select text by dragging with a mouse', (WidgetTester tester) async {
1795+
final TextEditingController controller = TextEditingController();
1796+
1797+
await tester.pumpWidget(
1798+
CupertinoApp(
1799+
home: Center(
1800+
child: CupertinoTextField(
1801+
dragStartBehavior: DragStartBehavior.down,
1802+
controller: controller,
1803+
style: const TextStyle(
1804+
fontFamily: 'Ahem',
1805+
fontSize: 10.0,
1806+
),
1807+
),
1808+
),
1809+
),
1810+
);
1811+
1812+
const String testValue = 'abc def ghi';
1813+
await tester.enterText(find.byType(CupertinoTextField), testValue);
1814+
// Skip past scrolling animation.
1815+
await tester.pump();
1816+
await tester.pump(const Duration(milliseconds: 200));
1817+
1818+
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
1819+
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
1820+
1821+
final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
1822+
await tester.pump();
1823+
await gesture.moveTo(gPos);
1824+
await tester.pump();
1825+
await gesture.up();
1826+
await tester.pumpAndSettle();
1827+
1828+
expect(controller.selection.baseOffset, testValue.indexOf('e'));
1829+
expect(controller.selection.extentOffset, testValue.indexOf('g'));
1830+
});
1831+
1832+
testWidgets('Continuous dragging does not cause flickering', (WidgetTester tester) async {
1833+
int selectionChangedCount = 0;
1834+
const String testValue = 'abc def ghi';
1835+
final TextEditingController controller = TextEditingController(text: testValue);
1836+
1837+
controller.addListener(() {
1838+
selectionChangedCount++;
1839+
});
1840+
1841+
await tester.pumpWidget(
1842+
CupertinoApp(
1843+
home: Center(
1844+
child: CupertinoTextField(
1845+
dragStartBehavior: DragStartBehavior.down,
1846+
controller: controller,
1847+
style: const TextStyle(
1848+
fontFamily: 'Ahem',
1849+
fontSize: 10.0,
1850+
),
1851+
),
1852+
),
1853+
),
1854+
);
1855+
1856+
final Offset cPos = textOffsetToPosition(tester, 2); // Index of 'c'.
1857+
final Offset gPos = textOffsetToPosition(tester, 8); // Index of 'g'.
1858+
final Offset hPos = textOffsetToPosition(tester, 9); // Index of 'h'.
1859+
1860+
// Drag from 'c' to 'g'.
1861+
final TestGesture gesture = await tester.startGesture(cPos, kind: PointerDeviceKind.mouse);
1862+
await tester.pump();
1863+
await gesture.moveTo(gPos);
1864+
await tester.pumpAndSettle();
1865+
1866+
expect(selectionChangedCount, isNonZero);
1867+
selectionChangedCount = 0;
1868+
expect(controller.selection.baseOffset, 2);
1869+
expect(controller.selection.extentOffset, 8);
1870+
1871+
// Tiny movement shouldn't cause text selection to change.
1872+
await gesture.moveTo(gPos + const Offset(4.0, 0.0));
1873+
await tester.pumpAndSettle();
1874+
expect(selectionChangedCount, 0);
1875+
1876+
// Now a text selection change will occur after a significant movement.
1877+
await gesture.moveTo(hPos);
1878+
await tester.pump();
1879+
await gesture.up();
1880+
await tester.pumpAndSettle();
1881+
1882+
expect(selectionChangedCount, 1);
1883+
expect(controller.selection.baseOffset, 2);
1884+
expect(controller.selection.extentOffset, 9);
1885+
});
1886+
17941887
testWidgets(
17951888
'text field respects theme',
17961889
(WidgetTester tester) async {

0 commit comments

Comments
 (0)