diff --git a/packages/flutter/lib/src/widgets/tap_and_drag_gestures.dart b/packages/flutter/lib/src/widgets/tap_and_drag_gestures.dart index 4bf60f95ef..51a8435b2f 100644 --- a/packages/flutter/lib/src/widgets/tap_and_drag_gestures.dart +++ b/packages/flutter/lib/src/widgets/tap_and_drag_gestures.dart @@ -539,6 +539,9 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer { @override void addAllowedPointer(PointerDownEvent event) { super.addAllowedPointer(event); + if (_consecutiveTapTimer != null && !_consecutiveTapTimer!.isActive) { + _tapTrackerReset(); + } if (maxConsecutiveTap == _consecutiveTapCount) { _tapTrackerReset(); } @@ -623,7 +626,7 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer { } void _consecutiveTapTimerStart() { - _consecutiveTapTimer ??= Timer(kDoubleTapTimeout, _tapTrackerReset); + _consecutiveTapTimer ??= Timer(kDoubleTapTimeout, _consecutiveTapTimerTimeout); } void _consecutiveTapTimerStop() { @@ -633,6 +636,13 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer { } } + void _consecutiveTapTimerTimeout() { + // The consecutive tap timer may time out before a tap down/tap up event is + // fired. In this case we should not reset the tap tracker state immediately. + // Instead we should reset the tap tracker on the next call to [addAllowedPointer], + // if the timer is no longer active. + } + void _tapTrackerReset() { // The timer has timed out, i.e. the time between a [PointerUpEvent] and the subsequent // [PointerDownEvent] exceeded the duration of [kDoubleTapTimeout], so the tap belonging diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 01df2af483..9c108dda1a 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -2147,6 +2147,52 @@ void main() { variant: TargetPlatformVariant.mobile(), ); + testWidgets('Can select text with a mouse when wrapped in a GestureDetector with tap/double tap callbacks', (WidgetTester tester) async { + // This is a regression test for https://github.com/flutter/flutter/issues/129161. + final TextEditingController controller = TextEditingController(); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: GestureDetector( + onTap: () {}, + onDoubleTap: () {}, + child: TextField( + dragStartBehavior: DragStartBehavior.down, + controller: controller, + ), + ), + ), + ), + ); + + const String testValue = 'abc def ghi'; + await tester.enterText(find.byType(TextField), testValue); + await skipPastScrollingAnimation(tester); + + 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.up(); + // This is to allow the GestureArena to decide a winner between TapGestureRecognizer, + // DoubleTapGestureRecognizer, and BaseTapAndDragGestureRecognizer. + await tester.pumpAndSettle(kDoubleTapTimeout); + expect(controller.selection.isCollapsed, true); + expect(controller.selection.baseOffset, testValue.indexOf('e')); + + await gesture.down(ePos); + 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')); + }, variant: TargetPlatformVariant.desktop()); + testWidgets('Can select text by dragging with a mouse', (WidgetTester tester) async { final TextEditingController controller = TextEditingController(); diff --git a/packages/flutter/test/widgets/tap_and_drag_gestures_test.dart b/packages/flutter/test/widgets/tap_and_drag_gestures_test.dart index 972e7c08fa..33027beeee 100644 --- a/packages/flutter/test/widgets/tap_and_drag_gestures_test.dart +++ b/packages/flutter/test/widgets/tap_and_drag_gestures_test.dart @@ -694,6 +694,44 @@ void main() { 'panend#2']); }); + // This is a regression test for https://github.com/flutter/flutter/issues/129161. + testGesture('Beats TapGestureRecognizer and DoubleTapGestureRecognizer when the pointer has not moved and this recognizer is the first in the arena', (GestureTester tester) { + setUpTapAndPanGestureRecognizer(); + + final TapGestureRecognizer taps = TapGestureRecognizer() + ..onTapDown = (TapDownDetails details) { + events.add('tapdown'); + } + ..onTapUp = (TapUpDetails details) { + events.add('tapup'); + } + ..onTapCancel = () { + events.add('tapscancel'); + }; + + final DoubleTapGestureRecognizer doubleTaps = DoubleTapGestureRecognizer() + ..onDoubleTapDown = (TapDownDetails details) { + events.add('doubletapdown'); + } + ..onDoubleTap = () { + events.add('doubletapup'); + } + ..onDoubleTapCancel = () { + events.add('doubletapcancel'); + }; + + tapAndDrag.addPointer(down1); + taps.addPointer(down1); + doubleTaps.addPointer(down1); + tester.closeArena(1); + tester.route(down1); + tester.route(up1); + GestureBinding.instance.gestureArena.sweep(1); + // Wait for GestureArena to resolve itself. + tester.async.elapse(kDoubleTapTimeout); + expect(events, ['down#1', 'up#1']); + }); + testGesture('Beats TapGestureRecognizer when the pointer has not moved and this recognizer is the first in the arena', (GestureTester tester) { setUpTapAndPanGestureRecognizer();