From e708aa64bddd2c2d9ee202291b495c60362d684a Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Fri, 11 Jun 2021 20:31:20 -0700 Subject: [PATCH] Reland: MouseRegion enter/exit event can be triggered with button pressed (#83253) * Revert "Revert "MouseRegion enter/exit event can be triggered with button pressed (#81148)" (#81557)" --- .../flutter/lib/src/rendering/binding.dart | 14 ++-- .../lib/src/rendering/mouse_tracker.dart | 15 ++-- .../test/widgets/hit_testing_test.dart | 72 +++++++++++++++++++ .../test/widgets/mouse_region_test.dart | 36 ++++++++++ 4 files changed, 125 insertions(+), 12 deletions(-) diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart index 56d061f524..d889e249d3 100644 --- a/packages/flutter/lib/src/rendering/binding.dart +++ b/packages/flutter/lib/src/rendering/binding.dart @@ -278,12 +278,14 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture @override // from GestureBinding void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) { - if (hitTestResult != null || - event is PointerAddedEvent || - event is PointerRemovedEvent) { - assert(event.position != null); - _mouseTracker!.updateWithEvent(event, () => hitTestResult ?? renderView.hitTestMouseTrackers(event.position)); - } + _mouseTracker!.updateWithEvent( + event, + // Enter and exit events should be triggered with or without buttons + // pressed. When the button is pressed, normal hit test uses a cached + // result, but MouseTracker requires that the hit test is re-executed to + // update the hovering events. + () => (hitTestResult == null || event is PointerMoveEvent) ? renderView.hitTestMouseTrackers(event.position) : hitTestResult, + ); super.dispatchEvent(event, hitTestResult); } diff --git a/packages/flutter/lib/src/rendering/mouse_tracker.dart b/packages/flutter/lib/src/rendering/mouse_tracker.dart index 965a064fe0..c72dd1c668 100644 --- a/packages/flutter/lib/src/rendering/mouse_tracker.dart +++ b/packages/flutter/lib/src/rendering/mouse_tracker.dart @@ -289,17 +289,20 @@ class MouseTracker extends ChangeNotifier { /// Trigger a device update with a new event and its corresponding hit test /// result. /// - /// The [updateWithEvent] indicates that an event has been observed, and - /// is called during the handler of the event. The `getResult` should return - /// the hit test result at the position of the event. + /// The [updateWithEvent] indicates that an event has been observed, and is + /// called during the handler of the event. It is typically called by + /// [RendererBinding], and should be called with all events received, and let + /// [MouseTracker] filter which to react to. + /// + /// The `getResult` is a function to return the hit test result at the + /// position of the event. It should not simply return cached hit test + /// result, because the cache does not change throughout a tap sequence. void updateWithEvent(PointerEvent event, ValueGetter getResult) { - assert(event != null); - final HitTestResult result = event is PointerRemovedEvent ? HitTestResult() : getResult(); - assert(result != null); if (event.kind != PointerDeviceKind.mouse) return; if (event is PointerSignalEvent) return; + final HitTestResult result = event is PointerRemovedEvent ? HitTestResult() : getResult(); final int device = event.device; final _MouseState? existingState = _mouseStates[device]; if (!_shouldMarkStateDirty(existingState, event)) diff --git a/packages/flutter/test/widgets/hit_testing_test.dart b/packages/flutter/test/widgets/hit_testing_test.dart index 30a1f2470d..a73a465ad7 100644 --- a/packages/flutter/test/widgets/hit_testing_test.dart +++ b/packages/flutter/test/widgets/hit_testing_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -13,4 +14,75 @@ void main() { expect(result, hasOneLineDescription); expect(result.path.first, hasOneLineDescription); }); + + testWidgets('A mouse click should only cause one hit test', (WidgetTester tester) async { + int hitCount = 0; + await tester.pumpWidget( + _HitTestCounter( + onHitTestCallback: () { hitCount += 1; }, + child: Container(), + ), + ); + + final TestGesture gesture = + await tester.startGesture(tester.getCenter(find.byType(_HitTestCounter)), kind: PointerDeviceKind.mouse); + addTearDown(gesture.removePointer); + await gesture.up(); + + expect(hitCount, 1); + }); + + testWidgets('Non-mouse events should not cause movement hit tests', (WidgetTester tester) async { + int hitCount = 0; + await tester.pumpWidget( + _HitTestCounter( + onHitTestCallback: () { hitCount += 1; }, + child: Container(), + ), + ); + + final TestGesture gesture = + await tester.startGesture(tester.getCenter(find.byType(_HitTestCounter)), kind: PointerDeviceKind.touch); + await gesture.moveBy(const Offset(1, 1)); + await gesture.up(); + + expect(hitCount, 1); + }); +} + + +// The [_HitTestCounter] invokes [onHitTestCallback] every time +// [hitTestChildren] is called. +class _HitTestCounter extends SingleChildRenderObjectWidget { + const _HitTestCounter({ + Key? key, + required Widget child, + required this.onHitTestCallback, + }) : super(key: key, child: child); + + final VoidCallback? onHitTestCallback; + + @override + _RenderHitTestCounter createRenderObject(BuildContext context) { + return _RenderHitTestCounter() + .._onHitTestCallback = onHitTestCallback; + } + + @override + void updateRenderObject( + BuildContext context, + _RenderHitTestCounter renderObject, + ) { + renderObject._onHitTestCallback = onHitTestCallback; + } +} + +class _RenderHitTestCounter extends RenderProxyBox { + VoidCallback? _onHitTestCallback; + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + _onHitTestCallback?.call(); + return super.hitTestChildren(result, position: position); + } } diff --git a/packages/flutter/test/widgets/mouse_region_test.dart b/packages/flutter/test/widgets/mouse_region_test.dart index d9eec082fb..6ade4bbef3 100644 --- a/packages/flutter/test/widgets/mouse_region_test.dart +++ b/packages/flutter/test/widgets/mouse_region_test.dart @@ -76,6 +76,42 @@ class _HoverFeedbackState extends State { } void main() { + testWidgets('onEnter and onExit can be triggered with mouse buttons pressed', (WidgetTester tester) async { + PointerEnterEvent? enter; + PointerExitEvent? exit; + await tester.pumpWidget(Center( + child: MouseRegion( + child: Container( + color: const Color.fromARGB(0xff, 0xff, 0x00, 0x00), + width: 100.0, + height: 100.0, + ), + onEnter: (PointerEnterEvent details) => enter = details, + onExit: (PointerExitEvent details) => exit = details, + ), + )); + + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, buttons: kPrimaryMouseButton); + await gesture.addPointer(location: Offset.zero); + await gesture.down(Offset.zero); // Press the mouse button. + addTearDown(gesture.removePointer); + await tester.pump(); + enter = null; + exit = null; + // Trigger the enter event. + await gesture.moveTo(const Offset(400.0, 300.0)); + expect(enter, isNotNull); + expect(enter!.position, equals(const Offset(400.0, 300.0))); + expect(enter!.localPosition, equals(const Offset(50.0, 50.0))); + expect(exit, isNull); + + // Trigger the exit event. + await gesture.moveTo(const Offset(1.0, 1.0)); + expect(exit, isNotNull); + expect(exit!.position, equals(const Offset(1.0, 1.0))); + expect(exit!.localPosition, equals(const Offset(-349.0, -249.0))); + }); + testWidgets('detects pointer enter', (WidgetTester tester) async { PointerEnterEvent? enter; PointerHoverEvent? move;