diff --git a/packages/flutter/lib/src/cupertino/text_field.dart b/packages/flutter/lib/src/cupertino/text_field.dart index 7bce145ab5..11217e0313 100644 --- a/packages/flutter/lib/src/cupertino/text_field.dart +++ b/packages/flutter/lib/src/cupertino/text_field.dart @@ -533,6 +533,28 @@ class _CupertinoTextFieldState extends State with AutomaticK _editableTextKey.currentState.showToolbar(); } + void _handleMouseDragSelectionStart(DragStartDetails details) { + _renderEditable.selectPositionAt( + from: details.globalPosition, + cause: SelectionChangedCause.drag, + ); + } + + void _handleMouseDragSelectionUpdate( + DragStartDetails startDetails, + DragUpdateDetails updateDetails, + ) { + _renderEditable.selectPositionAt( + from: startDetails.globalPosition, + to: updateDetails.globalPosition, + cause: SelectionChangedCause.drag, + ); + } + + void _handleMouseDragSelectionEnd(DragEndDetails details) { + _requestKeyboard(); + } + void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) { if (cause == SelectionChangedCause.longPress) { _editableTextKey.currentState?.bringIntoView(selection.base); @@ -742,6 +764,9 @@ class _CupertinoTextFieldState extends State with AutomaticK onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate, onSingleLongTapEnd: _handleSingleLongTapEnd, onDoubleTapDown: _handleDoubleTapDown, + onDragSelectionStart: _handleMouseDragSelectionStart, + onDragSelectionUpdate: _handleMouseDragSelectionUpdate, + onDragSelectionEnd: _handleMouseDragSelectionEnd, behavior: HitTestBehavior.translucent, child: _addTextDependentAttachments(paddedEditable, textStyle), ), diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index 639948884c..23999dc88b 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart @@ -743,7 +743,7 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi } } - void _handleDragSelectionStart(DragStartDetails details) { + void _handleMouseDragSelectionStart(DragStartDetails details) { _renderEditable.selectPositionAt( from: details.globalPosition, cause: SelectionChangedCause.drag, @@ -751,7 +751,7 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi _startSplash(details.globalPosition); } - void _handleDragSelectionUpdate( + void _handleMouseDragSelectionUpdate( DragStartDetails startDetails, DragUpdateDetails updateDetails, ) { @@ -930,8 +930,8 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate, onSingleLongTapEnd: _handleSingleLongTapEnd, onDoubleTapDown: _handleDoubleTapDown, - onDragSelectionStart: _handleDragSelectionStart, - onDragSelectionUpdate: _handleDragSelectionUpdate, + onDragSelectionStart: _handleMouseDragSelectionStart, + onDragSelectionUpdate: _handleMouseDragSelectionUpdate, behavior: HitTestBehavior.translucent, child: child, ), diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart index 883c06c0fa..eddaa6bab3 100644 --- a/packages/flutter/test/cupertino/text_field_test.dart +++ b/packages/flutter/test/cupertino/text_field_test.dart @@ -8,7 +8,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart' show DragStartBehavior; +import 'package:flutter/gestures.dart' show DragStartBehavior, PointerDeviceKind; import 'package:flutter_test/flutter_test.dart'; class MockClipboard { @@ -1791,6 +1791,99 @@ void main() { expect(controller.selection.extentOffset, 5); }); + testWidgets('Can select text by dragging with a mouse', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController(); + + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: CupertinoTextField( + dragStartBehavior: DragStartBehavior.down, + controller: controller, + style: const TextStyle( + fontFamily: 'Ahem', + fontSize: 10.0, + ), + ), + ), + ), + ); + + const String testValue = 'abc def ghi'; + await tester.enterText(find.byType(CupertinoTextField), testValue); + // Skip past scrolling animation. + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); + + final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); + final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g')); + + final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse); + await tester.pump(); + await gesture.moveTo(gPos); + await tester.pump(); + await gesture.up(); + await tester.pumpAndSettle(); + + expect(controller.selection.baseOffset, testValue.indexOf('e')); + expect(controller.selection.extentOffset, testValue.indexOf('g')); + }); + + testWidgets('Continuous dragging does not cause flickering', (WidgetTester tester) async { + int selectionChangedCount = 0; + const String testValue = 'abc def ghi'; + final TextEditingController controller = TextEditingController(text: testValue); + + controller.addListener(() { + selectionChangedCount++; + }); + + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: CupertinoTextField( + dragStartBehavior: DragStartBehavior.down, + controller: controller, + style: const TextStyle( + fontFamily: 'Ahem', + fontSize: 10.0, + ), + ), + ), + ), + ); + + final Offset cPos = textOffsetToPosition(tester, 2); // Index of 'c'. + final Offset gPos = textOffsetToPosition(tester, 8); // Index of 'g'. + final Offset hPos = textOffsetToPosition(tester, 9); // Index of 'h'. + + // Drag from 'c' to 'g'. + final TestGesture gesture = await tester.startGesture(cPos, kind: PointerDeviceKind.mouse); + await tester.pump(); + await gesture.moveTo(gPos); + await tester.pumpAndSettle(); + + expect(selectionChangedCount, isNonZero); + selectionChangedCount = 0; + expect(controller.selection.baseOffset, 2); + expect(controller.selection.extentOffset, 8); + + // Tiny movement shouldn't cause text selection to change. + await gesture.moveTo(gPos + const Offset(4.0, 0.0)); + await tester.pumpAndSettle(); + expect(selectionChangedCount, 0); + + // Now a text selection change will occur after a significant movement. + await gesture.moveTo(hPos); + await tester.pump(); + await gesture.up(); + await tester.pumpAndSettle(); + + expect(selectionChangedCount, 1); + expect(controller.selection.baseOffset, 2); + expect(controller.selection.extentOffset, 9); + }); + testWidgets( 'text field respects theme', (WidgetTester tester) async {