diff --git a/packages/flutter/lib/src/gestures/multitap.dart b/packages/flutter/lib/src/gestures/multitap.dart index ea83c1f0cc..3e805c2193 100644 --- a/packages/flutter/lib/src/gestures/multitap.dart +++ b/packages/flutter/lib/src/gestures/multitap.dart @@ -478,6 +478,7 @@ class MultiTapGestureRecognizer extends GestureRecognizer { if (onTapUp != null) invokeCallback('onTapUp', () { onTapUp!(pointer, TapUpDetails( + kind: getKindForPointer(pointer), localPosition: position.local, globalPosition: position.global, )); diff --git a/packages/flutter/lib/src/gestures/tap.dart b/packages/flutter/lib/src/gestures/tap.dart index 8433db57cc..6f9d5fe411 100644 --- a/packages/flutter/lib/src/gestures/tap.dart +++ b/packages/flutter/lib/src/gestures/tap.dart @@ -59,6 +59,7 @@ typedef GestureTapDownCallback = void Function(TapDownDetails details); class TapUpDetails { /// The [globalPosition] argument must not be null. TapUpDetails({ + required this.kind, this.globalPosition = Offset.zero, Offset? localPosition, }) : assert(globalPosition != null), @@ -69,6 +70,9 @@ class TapUpDetails { /// The local position at which the pointer contacted the screen. final Offset localPosition; + + /// The kind of the device that initiated the event. + final PointerDeviceKind kind; } /// Signature for when a pointer that will trigger a tap has stopped contacting @@ -512,6 +516,7 @@ class TapGestureRecognizer extends BaseTapGestureRecognizer { @override void handleTapUp({ required PointerDownEvent down, required PointerUpEvent up}) { final TapUpDetails details = TapUpDetails( + kind: up.kind, globalPosition: up.position, localPosition: up.localPosition, ); diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index 2080d2cfbd..5bd42c08ce 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart @@ -93,7 +93,20 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete switch (Theme.of(_state.context).platform) { case TargetPlatform.iOS: case TargetPlatform.macOS: - renderEditable.selectWordEdge(cause: SelectionChangedCause.tap); + switch (details.kind) { + case PointerDeviceKind.mouse: + case PointerDeviceKind.stylus: + case PointerDeviceKind.invertedStylus: + // Precise devices should place the cursor at a precise position. + renderEditable.selectPosition(cause: SelectionChangedCause.tap); + break; + case PointerDeviceKind.touch: + case PointerDeviceKind.unknown: + // On macOS/iOS/iPadOS a touch tap places the cursor at the edge + // of the word. + renderEditable.selectWordEdge(cause: SelectionChangedCause.tap); + break; + } break; case TargetPlatform.android: case TargetPlatform.fuchsia: diff --git a/packages/flutter/lib/src/widgets/gesture_detector.dart b/packages/flutter/lib/src/widgets/gesture_detector.dart index 1285eb8189..648ba84cf0 100644 --- a/packages/flutter/lib/src/widgets/gesture_detector.dart +++ b/packages/flutter/lib/src/widgets/gesture_detector.dart @@ -1260,7 +1260,7 @@ class _DefaultSemanticsGestureDelegate extends SemanticsGestureDelegate { if (tap.onTapDown != null) tap.onTapDown(TapDownDetails()); if (tap.onTapUp != null) - tap.onTapUp(TapUpDetails()); + tap.onTapUp(TapUpDetails(kind: PointerDeviceKind.unknown)); if (tap.onTap != null) tap.onTap(); }; diff --git a/packages/flutter/test/gestures/tap_test.dart b/packages/flutter/test/gestures/tap_test.dart index 7ef3032367..d76dd6dc91 100644 --- a/packages/flutter/test/gestures/tap_test.dart +++ b/packages/flutter/test/gestures/tap_test.dart @@ -109,6 +109,35 @@ void main() { tap.dispose(); }); + testGesture('Details contain the correct device kind', (GestureTester tester) { + final TapGestureRecognizer tap = TapGestureRecognizer(); + + TapDownDetails lastDownDetails; + TapUpDetails lastUpDetails; + + tap.onTapDown = (TapDownDetails details) { + lastDownDetails = details; + }; + tap.onTapUp = (TapUpDetails details) { + lastUpDetails = details; + }; + + const PointerDownEvent mouseDown = + PointerDownEvent(pointer: 1, kind: PointerDeviceKind.mouse); + const PointerUpEvent mouseUp = + PointerUpEvent(pointer: 1, kind: PointerDeviceKind.mouse); + + tap.addPointer(mouseDown); + tester.closeArena(1); + tester.route(mouseDown); + expect(lastDownDetails.kind, PointerDeviceKind.mouse); + + tester.route(mouseUp); + expect(lastUpDetails.kind, PointerDeviceKind.mouse); + + tap.dispose(); + }); + testGesture('No duplicate tap events', (GestureTester tester) { final TapGestureRecognizer tap = TapGestureRecognizer(); diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index bc7f26c87c..5ae8c47eea 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -6110,6 +6110,41 @@ void main() { expect(find.byType(CupertinoButton), findsNothing); }, variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS })); + testWidgets( + 'tap with a mouse does not move cursor to the edge of the word', + (WidgetTester tester) async { + final TextEditingController controller = TextEditingController( + text: 'Atwater Peel Sherbrooke Bonaventure', + ); + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: TextField( + controller: controller, + ), + ), + ), + ), + ); + + final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); + + final TestGesture gesture = await tester.startGesture( + textfieldStart + const Offset(50.0, 9.0), + pointer: 1, + kind: PointerDeviceKind.mouse, + ); + addTearDown(gesture.removePointer); + await gesture.up(); + + // Cursor at tap position, not at word edge. + expect( + controller.selection, + const TextSelection.collapsed(offset: 3, affinity: TextAffinity.downstream), + ); + }, variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS })); + testWidgets('tap moves cursor to the position tapped', (WidgetTester tester) async { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure',