diff --git a/packages/flutter/lib/src/gestures/binding.dart b/packages/flutter/lib/src/gestures/binding.dart index a2f2e43d4bb..696acb95799 100644 --- a/packages/flutter/lib/src/gestures/binding.dart +++ b/packages/flutter/lib/src/gestures/binding.dart @@ -173,8 +173,9 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H @override // from HitTestDispatcher void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) { assert(!locked); - // No hit test information implies that this is a hover or pointer - // add/remove event. + // No hit test information implies that this is a pointer hover or + // add/remove event. These events are specially routed here; other events + // will be routed through the `handleEvent` below. if (hitTestResult == null) { assert(event is PointerHoverEvent || event is PointerAddedEvent || event is PointerRemovedEvent); try { diff --git a/packages/flutter/lib/src/gestures/events.dart b/packages/flutter/lib/src/gestures/events.dart index fb2b4bc1d59..c36828ba807 100644 --- a/packages/flutter/lib/src/gestures/events.dart +++ b/packages/flutter/lib/src/gestures/events.dart @@ -869,7 +869,7 @@ class PointerHoverEvent extends PointerEvent { /// * [PointerExitEvent], which reports when the pointer has left an object. /// * [PointerMoveEvent], which reports movement while the pointer is in /// contact with the device. -/// * [Listener.onPointerEnter], which allows callers to be notified of these +/// * [MouseRegion.onEnter], which allows callers to be notified of these /// events in a widget tree. class PointerEnterEvent extends PointerEvent { /// Creates a pointer enter event. @@ -1020,7 +1020,7 @@ class PointerEnterEvent extends PointerEvent { /// * [PointerEnterEvent], which reports when the pointer has entered an object. /// * [PointerMoveEvent], which reports movement while the pointer is in /// contact with the device. -/// * [Listener.onPointerExit], which allows callers to be notified of these +/// * [MouseRegion.onExit], which allows callers to be notified of these /// events in a widget tree. class PointerExitEvent extends PointerEvent { /// Creates a pointer exit event. diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart index 156e4d97522..c01e35064d4 100644 --- a/packages/flutter/lib/src/rendering/binding.dart +++ b/packages/flutter/lib/src/rendering/binding.dart @@ -248,7 +248,19 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture @visibleForTesting void initMouseTracker([MouseTracker tracker]) { _mouseTracker?.dispose(); - _mouseTracker = tracker ?? MouseTracker(pointerRouter, renderView.hitTestMouseTrackers); + _mouseTracker = tracker ?? MouseTracker(); + } + + @override // from GestureBinding + void dispatchEvent(PointerEvent event, HitTestResult hitTestResult) { + if (hitTestResult != null || + event is PointerHoverEvent || + event is PointerAddedEvent || + event is PointerRemovedEvent) { + _mouseTracker.updateWithEvent(event, + () => hitTestResult ?? renderView.hitTestMouseTrackers(event.position)); + } + super.dispatchEvent(event, hitTestResult); } void _handleSemanticsEnabledChanged() { @@ -284,7 +296,24 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture void _handlePersistentFrameCallback(Duration timeStamp) { drawFrame(); - _mouseTracker.schedulePostFrameCheck(); + _scheduleMouseTrackerUpdate(); + } + + bool _debugMouseTrackerUpdateScheduled = false; + void _scheduleMouseTrackerUpdate() { + assert(!_debugMouseTrackerUpdateScheduled); + assert(() { + _debugMouseTrackerUpdateScheduled = true; + return true; + }()); + SchedulerBinding.instance.addPostFrameCallback((Duration duration) { + assert(_debugMouseTrackerUpdateScheduled); + assert(() { + _debugMouseTrackerUpdateScheduled = false; + return true; + }()); + _mouseTracker.updateAllDevices(renderView.hitTestMouseTrackers); + }); } int _firstFrameDeferredCount = 0; diff --git a/packages/flutter/lib/src/rendering/mouse_tracking.dart b/packages/flutter/lib/src/rendering/mouse_tracking.dart index 515c76a3efd..23ec3da6895 100644 --- a/packages/flutter/lib/src/rendering/mouse_tracking.dart +++ b/packages/flutter/lib/src/rendering/mouse_tracking.dart @@ -9,7 +9,6 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/scheduler.dart'; import 'package:vector_math/vector_math_64.dart' show Matrix4; @@ -121,7 +120,6 @@ class MouseTrackerAnnotation with Diagnosticable { 'callbacks', { 'enter': onEnter, - 'hover': onHover, 'exit': onExit, }, ifEmpty: '', @@ -134,7 +132,7 @@ class MouseTrackerAnnotation with Diagnosticable { /// /// It is used by the [BaseMouseTracker] to fetch annotations for the mouse /// position. -typedef MouseDetectorAnnotationFinder = LinkedHashMap Function(Offset offset); +typedef MouseDetectorAnnotationFinder = HitTestResult Function(Offset offset); // Various states of a connected mouse device used by [BaseMouseTracker]. class _MouseState { @@ -269,107 +267,34 @@ class MouseTrackerUpdateDetails with Diagnosticable { /// A base class that tracks the relationship between mouse devices and /// [MouseTrackerAnnotation]s. /// -/// A _device update_ is defined as an event that changes the relationship -/// between mouse devices and [MouseTrackerAnnotation]s. Subclasses should -/// override [handleDeviceUpdate] to process the updates. +/// An event (not necessarily a pointer event) that might change the relationship +/// between mouse devices and [MouseTrackerAnnotation]s is called a _device +/// update_. +/// +/// [MouseTracker] is notified of device updates by [updateWithEvent] or +/// [updateAllDevices], and processes effects as defined in [handleDeviceUpdate] +/// by subclasses. /// /// This class is a [ChangeNotifier] that notifies its listeners if the value of /// [mouseIsConnected] changes. /// -/// ### States and device updates -/// -/// The state of [BaseMouseTracker] consists of two parts: -/// -/// * The mouse devices that are connected. -/// * In which annotations each device is contained. -/// -/// The states remain stable most of the time, and are only changed at the -/// following moments: -/// -/// * An eligible [PointerEvent] has been observed, e.g. a device is added, -/// removed, or moved. In this case, the state related to this device will -/// be immediately updated, and triggers [handleDeviceUpdate] on this device. -/// * A frame has been painted. In this case, a callback will be scheduled for -/// the upcoming post-frame phase to update all devices, and triggers -/// [handleDeviceUpdate] on each device separately. -/// /// See also: /// /// * [MouseTracker], which is a subclass of [BaseMouseTracker] with definition /// of how to process mouse event callbacks and mouse cursors. /// * [MouseTrackerCursorMixin], which is a mixin for [BaseMouseTracker] that /// defines how to process mouse cursors. -class BaseMouseTracker extends ChangeNotifier { - /// Creates a [BaseMouseTracker] to keep track of mouse locations. - /// - /// The first parameter is a [PointerRouter], which [BaseMouseTracker] will - /// subscribe to and receive events from. Usually it is the global singleton - /// instance [GestureBinding.pointerRouter]. - /// - /// The second parameter is a function with which the [BaseMouseTracker] can - /// search for [MouseTrackerAnnotation]s at a given position. - /// Usually it is [Layer.findAllAnnotations] of the root layer. - /// - /// All of the parameters must be non-null. - BaseMouseTracker(this._router, this.annotationFinder) - : assert(_router != null), - assert(annotationFinder != null) { - _router.addGlobalRoute(_handleEvent); - } - - @override - void dispose() { - super.dispose(); - _router.removeGlobalRoute(_handleEvent); - } - - /// Find annotations at a given offset in global logical coordinate space - /// in visual order from front to back. - /// - /// [BaseMouseTracker] uses this callback to know which annotations are - /// affected by each device. - /// - /// The annotations should be returned in visual order from front to - /// back, so that the callbacks are called in an correct order. - final MouseDetectorAnnotationFinder annotationFinder; - - // The pointer router that the mouse tracker listens to, and receives new - // mouse events from. - final PointerRouter _router; - - bool _hasScheduledPostFrameCheck = false; - /// Mark all devices as dirty, and schedule a callback that is executed in the - /// upcoming post-frame phase to check their updates. - /// - /// Checking a device means to collect the annotations that the pointer - /// hovers, and triggers necessary callbacks accordingly. - /// - /// Although the actual callback belongs to the scheduler's post-frame phase, - /// this method must be called in persistent callback phase to ensure that - /// the callback is scheduled after every frame, since every frame can change - /// the position of annotations. Typically the method is called by - /// [RendererBinding]'s drawing method. - void schedulePostFrameCheck() { - assert(SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks); - assert(!_debugDuringDeviceUpdate); - if (!mouseIsConnected) - return; - if (!_hasScheduledPostFrameCheck) { - _hasScheduledPostFrameCheck = true; - SchedulerBinding.instance.addPostFrameCallback((Duration duration) { - assert(_hasScheduledPostFrameCheck); - _hasScheduledPostFrameCheck = false; - _updateAllDevices(); - }); - } - } - +abstract class BaseMouseTracker extends ChangeNotifier { /// Whether or not at least one mouse is connected and has produced events. bool get mouseIsConnected => _mouseStates.isNotEmpty; // Tracks the state of connected mouse devices. // - // It is the source of truth for the list of connected mouse devices. + // It is the source of truth for the list of connected mouse devices, and is + // consists of two parts: + // + // * The mouse devices that are connected. + // * In which annotations each device is contained. final Map _mouseStates = {}; // Used to wrap any procedure that might change `mouseIsConnected`. @@ -420,27 +345,42 @@ class BaseMouseTracker extends ChangeNotifier { || lastEvent.position != event.position; } + LinkedHashMap _hitTestResultToAnnotations(HitTestResult result) { + assert(result != null); + final LinkedHashMap annotations = {} + as LinkedHashMap; + for (final HitTestEntry entry in result.path) { + if (entry.target is MouseTrackerAnnotation) { + annotations[entry.target as MouseTrackerAnnotation] = entry.transform; + } + } + return annotations; + } + // Find the annotations that is hovered by the device of the `state`, and // their respective global transform matrices. // // If the device is not connected or not a mouse, an empty map is returned - // without calling `annotationFinder`. - LinkedHashMap _findAnnotations(_MouseState state) { + // without calling `hitTest`. + LinkedHashMap _findAnnotations(_MouseState state, MouseDetectorAnnotationFinder hitTest) { + assert(state != null); + assert(hitTest != null); final Offset globalPosition = state.latestEvent.position; final int device = state.device; if (!_mouseStates.containsKey(device)) return {} as LinkedHashMap; - return annotationFinder(globalPosition); + + return _hitTestResultToAnnotations(hitTest(globalPosition)); } /// A callback that is called on the update of a device. /// - /// This method should be called only by [BaseMouseTracker]. + /// This method should be called only by [BaseMouseTracker], each time when the + /// relationship between a device and annotations has changed. /// - /// Override this method to receive updates when the relationship between a - /// device and annotations have changed. Subclasses should override this method - /// to first call to their inherited [handleDeviceUpdate] method, and then - /// process the update as desired. + /// By default the [handleDeviceUpdate] does nothing effective. Subclasses + /// should override this method to first call to their inherited + /// [handleDeviceUpdate] method, and then process the update as desired. /// /// The update can be caused by two kinds of triggers: /// @@ -451,8 +391,6 @@ class BaseMouseTracker extends ChangeNotifier { /// Such calls occur after each new frame, during the post-frame callbacks, /// indicated by `details.triggeringEvent` being null. /// - /// This method is not triggered if the [MouseTrackerAnnotation] is mutated. - /// /// Calling of this method must be wrapped in `_deviceUpdatePhase`. @protected @mustCallSuper @@ -460,10 +398,19 @@ class BaseMouseTracker extends ChangeNotifier { assert(_debugDuringDeviceUpdate); } - // Handler for events coming from the PointerRouter. - // - // If the event marks the device dirty, update the device immediately. - void _handleEvent(PointerEvent event) { + /// 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] will generate the new state for the pointer based on + /// given information, and call [handleDeviceUpdate] based on the state changes. + 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) @@ -479,6 +426,7 @@ class BaseMouseTracker extends ChangeNotifier { // so that [mouseIsConnected], which is decided by `_mouseStates`, is // correct during the callbacks. if (existingState == null) { + assert(event is! PointerRemovedEvent); _mouseStates[device] = _MouseState(initialEvent: event); } else { assert(event is! PointerAddedEvent); @@ -488,7 +436,9 @@ class BaseMouseTracker extends ChangeNotifier { final _MouseState targetState = _mouseStates[device] ?? existingState; final PointerEvent lastEvent = targetState.replaceLatestEvent(event); - final LinkedHashMap nextAnnotations = _findAnnotations(targetState); + final LinkedHashMap nextAnnotations = event is PointerRemovedEvent ? + {} as LinkedHashMap : + _hitTestResultToAnnotations(result); final LinkedHashMap lastAnnotations = targetState.replaceAnnotations(nextAnnotations); handleDeviceUpdate(MouseTrackerUpdateDetails.byPointerEvent( @@ -501,15 +451,22 @@ class BaseMouseTracker extends ChangeNotifier { }); } - // Update all devices, despite observing no new events. - // - // This is called after a new frame, since annotations can be moved after - // every frame. - void _updateAllDevices() { + /// Trigger a device update for all detected devices. + /// + /// The [updateAllDevices] is typically called during the post frame phase, + /// indicating a frame has passed and all objects have potentially moved. The + /// `hitTest` is a function that can acquire the hit test result at a given + /// position, and must not be empty. + /// + /// For each connected device, the [updateAllDevices] will make a hit test on + /// the device's last seen position, generate the new state for the pointer + /// based on given information, and call [handleDeviceUpdate] based on the + /// state changes. + void updateAllDevices(MouseDetectorAnnotationFinder hitTest) { _deviceUpdatePhase(() { for (final _MouseState dirtyState in _mouseStates.values) { final PointerEvent lastEvent = dirtyState.latestEvent; - final LinkedHashMap nextAnnotations = _findAnnotations(dirtyState); + final LinkedHashMap nextAnnotations = _findAnnotations(dirtyState, hitTest); final LinkedHashMap lastAnnotations = dirtyState.replaceAnnotations(nextAnnotations); handleDeviceUpdate(MouseTrackerUpdateDetails.byNewFrame( @@ -612,19 +569,4 @@ mixin _MouseTrackerEventMixin on BaseMouseTracker { /// * [BaseMouseTracker], which introduces more details about the timing of /// device updates. class MouseTracker extends BaseMouseTracker with MouseTrackerCursorMixin, _MouseTrackerEventMixin { - /// Creates a [MouseTracker] to keep track of mouse locations. - /// - /// The first parameter is a [PointerRouter], which [MouseTracker] will - /// subscribe to and receive events from. Usually it is the global singleton - /// instance [GestureBinding.pointerRouter]. - /// - /// The second parameter is a function with which the [MouseTracker] can - /// search for [MouseTrackerAnnotation]s at a given position. - /// Usually it is [Layer.findAllAnnotations] of the root layer. - /// - /// All of the parameters must be non-null. - MouseTracker( - PointerRouter router, - MouseDetectorAnnotationFinder annotationFinder, - ) : super(router, annotationFinder); } diff --git a/packages/flutter/lib/src/rendering/platform_view.dart b/packages/flutter/lib/src/rendering/platform_view.dart index bc8f0bc5934..f6bb8019ade 100644 --- a/packages/flutter/lib/src/rendering/platform_view.dart +++ b/packages/flutter/lib/src/rendering/platform_view.dart @@ -12,7 +12,6 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/semantics.dart'; import 'package:flutter/services.dart'; -import 'binding.dart'; import 'box.dart'; import 'layer.dart'; import 'mouse_cursor.dart'; @@ -668,7 +667,7 @@ mixin _PlatformViewGestureMixin on RenderBox implements MouseTrackerAnnotation { if (value != _hitTestBehavior) { _hitTestBehavior = value; if (owner != null) - RendererBinding.instance.mouseTracker.schedulePostFrameCheck(); + markNeedsPaint(); } } PlatformViewHitTestBehavior _hitTestBehavior; diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 1b098ab8ef3..523f358e326 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -2762,20 +2762,16 @@ class RenderMouseRegion extends RenderProxyBox implements MouseTrackerAnnotation /// mouse region with no callbacks and cursor being [MouseCursor.defer]. The /// [cursor] must not be null. RenderMouseRegion({ - PointerEnterEventListener onEnter, - PointerHoverEventListener onHover, - PointerExitEventListener onExit, + this.onEnter, + this.onHover, + this.onExit, MouseCursor cursor = MouseCursor.defer, bool opaque = true, RenderBox child, }) : assert(opaque != null), assert(cursor != null), - _onEnter = onEnter, - _onHover = onHover, - _onExit = onExit, _cursor = cursor, _opaque = opaque, - _annotationIsActive = false, super(child); @protected @@ -2787,6 +2783,13 @@ class RenderMouseRegion extends RenderProxyBox implements MouseTrackerAnnotation return super.hitTest(result, position: position) && _opaque; } + @override + void handleEvent(PointerEvent event, HitTestEntry entry) { + assert(debugHandleEvent(event, entry)); + if (onHover != null && event is PointerHoverEvent) + return onHover(event); + } + /// Whether this object should prevent [RenderMouseRegion]s visually behind it /// from detecting the pointer, thus affecting how their [onHover], [onEnter], /// and [onExit] behave. @@ -2806,41 +2809,19 @@ class RenderMouseRegion extends RenderProxyBox implements MouseTrackerAnnotation set opaque(bool value) { if (_opaque != value) { _opaque = value; - // A repaint is needed in order to propagate the new value to - // AnnotatedRegionLayer via [paint]. - _markPropertyUpdated(mustRepaint: true); + // Trigger [MouseTracker]'s device update to recalculate mouse states. + markNeedsPaint(); } } @override - PointerEnterEventListener get onEnter => _onEnter; - PointerEnterEventListener _onEnter; - set onEnter(PointerEnterEventListener value) { - if (_onEnter != value) { - _onEnter = value; - _markPropertyUpdated(mustRepaint: false); - } - } + PointerEnterEventListener onEnter; @override - PointerHoverEventListener get onHover => _onHover; - PointerHoverEventListener _onHover; - set onHover(PointerHoverEventListener value) { - if (_onHover != value) { - _onHover = value; - _markPropertyUpdated(mustRepaint: false); - } - } + PointerHoverEventListener onHover; @override - PointerExitEventListener get onExit => _onExit; - PointerExitEventListener _onExit; - set onExit(PointerExitEventListener value) { - if (_onExit != value) { - _onExit = value; - _markPropertyUpdated(mustRepaint: false); - } - } + PointerExitEventListener onExit; @override MouseCursor get cursor => _cursor; @@ -2850,61 +2831,10 @@ class RenderMouseRegion extends RenderProxyBox implements MouseTrackerAnnotation _cursor = value; // A repaint is needed in order to trigger a device update of // [MouseTracker] so that this new value can be found. - _markPropertyUpdated(mustRepaint: true); - } - } - - // Call this method when a property has changed and might affect the - // `_annotationIsActive` bit. - // - // If `mustRepaint` is false, this method does NOT call `markNeedsPaint` - // unless the `_annotationIsActive` bit is changed. If there is a property - // that needs updating while `_annotationIsActive` stays true, make - // `mustRepaint` true. - // - // This method must not be called during `paint`. - void _markPropertyUpdated({@required bool mustRepaint}) { - assert(owner == null || !owner.debugDoingPaint); - final bool newAnnotationIsActive = ( - _onEnter != null || - _onHover != null || - _onExit != null || - _cursor != MouseCursor.defer || - opaque - ) && RendererBinding.instance.mouseTracker.mouseIsConnected; - _setAnnotationIsActive(newAnnotationIsActive); - if (mustRepaint) markNeedsPaint(); - } - - bool _annotationIsActive = false; - void _setAnnotationIsActive(bool value) { - final bool annotationWasActive = _annotationIsActive; - _annotationIsActive = value; - if (annotationWasActive != value) { - markNeedsPaint(); - markNeedsCompositingBitsUpdate(); } } - void _handleUpdatedMouseIsConnected() { - _markPropertyUpdated(mustRepaint: false); - } - - @override - void attach(PipelineOwner owner) { - super.attach(owner); - // Add a listener to listen for changes in mouseIsConnected. - RendererBinding.instance.mouseTracker.addListener(_handleUpdatedMouseIsConnected); - _markPropertyUpdated(mustRepaint: false); - } - - @override - void detach() { - RendererBinding.instance.mouseTracker.removeListener(_handleUpdatedMouseIsConnected); - super.detach(); - } - @override void performResize() { size = constraints.biggest; diff --git a/packages/flutter/lib/src/rendering/view.dart b/packages/flutter/lib/src/rendering/view.dart index 0a12b4e2bd6..dee0c07ad5a 100644 --- a/packages/flutter/lib/src/rendering/view.dart +++ b/packages/flutter/lib/src/rendering/view.dart @@ -4,7 +4,6 @@ // @dart = 2.8 -import 'dart:collection' show LinkedHashMap; import 'dart:developer'; import 'dart:io' show Platform; import 'dart:ui' as ui show Scene, SceneBuilder, Window; @@ -18,7 +17,6 @@ import 'binding.dart'; import 'box.dart'; import 'debug.dart'; import 'layer.dart'; -import 'mouse_tracking.dart'; import 'object.dart'; /// The layout constraints for the root render object. @@ -199,22 +197,13 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin /// /// * [Layer.findAllAnnotations], which is used by this method to find all /// [AnnotatedRegionLayer]s annotated for mouse tracking. - LinkedHashMap hitTestMouseTrackers(Offset position) { + HitTestResult hitTestMouseTrackers(Offset position) { // Layer hit testing is done using device pixels, so we have to convert // the logical coordinates of the event location back to device pixels // here. final BoxHitTestResult result = BoxHitTestResult(); - if (child != null) - child.hitTest(result, position: position); - result.add(HitTestEntry(this)); - final LinkedHashMap annotations = {} - as LinkedHashMap; - for (final HitTestEntry entry in result.path) { - if (entry.target is MouseTrackerAnnotation) { - annotations[entry.target as MouseTrackerAnnotation] = entry.transform; - } - } - return annotations; + hitTest(result, position: position); + return result; } @override diff --git a/packages/flutter/test/rendering/mouse_tracking_cursor_test.dart b/packages/flutter/test/rendering/mouse_tracking_cursor_test.dart index 462ddbd3bbb..e68f99d05b1 100644 --- a/packages/flutter/test/rendering/mouse_tracking_cursor_test.dart +++ b/packages/flutter/test/rendering/mouse_tracking_cursor_test.dart @@ -4,29 +4,27 @@ // @dart = 2.8 -import 'dart:collection' show LinkedHashMap; import 'dart:ui' as ui; import 'dart:ui' show PointerChange; import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import '../flutter_test_alternative.dart'; +import './mouse_tracking_test_utils.dart'; typedef MethodCallHandler = Future Function(MethodCall call); -_TestGestureFlutterBinding _binding = _TestGestureFlutterBinding(); +TestMouseTrackerFlutterBinding _binding = TestMouseTrackerFlutterBinding(); void _ensureTestGestureBinding() { - _binding ??= _TestGestureFlutterBinding(); + _binding ??= TestMouseTrackerFlutterBinding(); assert(GestureBinding.instance != null); } -typedef SimpleAnnotationFinder = Iterable Function(Offset offset); +typedef SimpleAnnotationFinder = Iterable Function(Offset offset); void main() { MethodCallHandler _methodCallHandler; @@ -44,15 +42,26 @@ void main() { return; } : cursorHandler; - final MouseTracker mouseTracker = MouseTracker( - GestureBinding.instance.pointerRouter, - (Offset offset) => LinkedHashMap.fromEntries( - annotationFinder(offset).map( - (MouseTrackerAnnotation annotation) => MapEntry(annotation, Matrix4.identity()), - ), - ), - ); - RendererBinding.instance.initMouseTracker(mouseTracker); + + _binding.setHitTest((BoxHitTestResult result, Offset position) { + for (final HitTestTarget target in annotationFinder(position)) { + result.addWithRawTransform( + transform: Matrix4.identity(), + position: null, + hitTest: (BoxHitTestResult result, Offset position) { + result.add(HitTestEntry(target)); + return true; + }, + ); + } + return true; + }); + } + + void dispatchRemoveDevice([int device = 0]) { + ui.window.onPointerDataPacket(ui.PointerDataPacket(data: [ + _pointerData(PointerChange.remove, const Offset(0.0, 0.0), device: device), + ])); } setUp(() { @@ -69,10 +78,10 @@ void main() { }); test('Should work on platforms that does not support mouse cursor', () async { - const MouseTrackerAnnotation annotation = MouseTrackerAnnotation(cursor: SystemMouseCursors.grabbing); + const TestAnnotationTarget annotation = TestAnnotationTarget(cursor: SystemMouseCursors.grabbing); _setUpMouseTracker( - annotationFinder: (Offset position) => [annotation], + annotationFinder: (Offset position) => [annotation], cursorHandler: (MethodCall call) async { return null; }, @@ -81,15 +90,16 @@ void main() { ui.window.onPointerDataPacket(ui.PointerDataPacket(data: [ _pointerData(PointerChange.add, const Offset(0.0, 0.0)), ])); + addTearDown(dispatchRemoveDevice); // Passes if no errors are thrown }); test('pointer is added and removed out of any annotations', () { final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[]; - MouseTrackerAnnotation annotation; + TestAnnotationTarget annotation; _setUpMouseTracker( - annotationFinder: (Offset position) => [if (annotation != null) annotation], + annotationFinder: (Offset position) => [if (annotation != null) annotation], logCursors: logCursors, ); @@ -104,7 +114,7 @@ void main() { logCursors.clear(); // Pointer moves into the annotation - annotation = const MouseTrackerAnnotation(cursor: SystemMouseCursors.grabbing); + annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing); ui.window.onPointerDataPacket(ui.PointerDataPacket(data: [ _pointerData(PointerChange.hover, const Offset(5.0, 0.0)), ])); @@ -115,7 +125,7 @@ void main() { logCursors.clear(); // Pointer moves within the annotation - annotation = const MouseTrackerAnnotation(cursor: SystemMouseCursors.grabbing); + annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing); ui.window.onPointerDataPacket(ui.PointerDataPacket(data: [ _pointerData(PointerChange.hover, const Offset(10.0, 0.0)), ])); @@ -146,14 +156,14 @@ void main() { test('pointer is added and removed in an annotation', () { final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[]; - MouseTrackerAnnotation annotation; + TestAnnotationTarget annotation; _setUpMouseTracker( - annotationFinder: (Offset position) => [if (annotation != null) annotation], + annotationFinder: (Offset position) => [if (annotation != null) annotation], logCursors: logCursors, ); // Pointer is added in the annotation. - annotation = const MouseTrackerAnnotation(cursor: SystemMouseCursors.grabbing); + annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing); ui.window.onPointerDataPacket(ui.PointerDataPacket(data: [ _pointerData(PointerChange.add, const Offset(0.0, 0.0)), ])); @@ -185,7 +195,7 @@ void main() { logCursors.clear(); // Pointer moves back into the annotation - annotation = const MouseTrackerAnnotation(cursor: SystemMouseCursors.grabbing); + annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing); ui.window.onPointerDataPacket(ui.PointerDataPacket(data: [ _pointerData(PointerChange.hover, const Offset(0.0, 0.0)), ])); @@ -206,9 +216,9 @@ void main() { test('pointer change caused by new frames', () { final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[]; - MouseTrackerAnnotation annotation; + TestAnnotationTarget annotation; _setUpMouseTracker( - annotationFinder: (Offset position) => [if (annotation != null) annotation], + annotationFinder: (Offset position) => [if (annotation != null) annotation], logCursors: logCursors, ); @@ -223,7 +233,7 @@ void main() { logCursors.clear(); // Synthesize a new frame while changing annotation - annotation = const MouseTrackerAnnotation(cursor: SystemMouseCursors.grabbing); + annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing); _binding.scheduleMouseTrackerPostFrameCheck(); _binding.flushPostFrameCallbacks(Duration.zero); @@ -233,7 +243,7 @@ void main() { logCursors.clear(); // Synthesize a new frame without changing annotation - annotation = const MouseTrackerAnnotation(cursor: SystemMouseCursors.grabbing); + annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing); _binding.scheduleMouseTrackerPostFrameCheck(); expect(logCursors, <_CursorUpdateDetails>[ @@ -251,16 +261,16 @@ void main() { test('The first annotation with non-deferring cursor is used', () { final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[]; - List annotations; + List annotations; _setUpMouseTracker( annotationFinder: (Offset position) sync* { yield* annotations; }, logCursors: logCursors, ); - annotations = [ - const MouseTrackerAnnotation(cursor: MouseCursor.defer), - const MouseTrackerAnnotation(cursor: SystemMouseCursors.click), - const MouseTrackerAnnotation(cursor: SystemMouseCursors.grabbing), + annotations = [ + const TestAnnotationTarget(cursor: MouseCursor.defer), + const TestAnnotationTarget(cursor: SystemMouseCursors.click), + const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing), ]; ui.window.onPointerDataPacket(ui.PointerDataPacket(data: [ _pointerData(PointerChange.add, const Offset(0.0, 0.0)), @@ -279,16 +289,16 @@ void main() { test('Annotations with deferring cursors are ignored', () { final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[]; - List annotations; + List annotations; _setUpMouseTracker( annotationFinder: (Offset position) sync* { yield* annotations; }, logCursors: logCursors, ); - annotations = [ - const MouseTrackerAnnotation(cursor: MouseCursor.defer), - const MouseTrackerAnnotation(cursor: MouseCursor.defer), - const MouseTrackerAnnotation(cursor: SystemMouseCursors.grabbing), + annotations = [ + const TestAnnotationTarget(cursor: MouseCursor.defer), + const TestAnnotationTarget(cursor: MouseCursor.defer), + const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing), ]; ui.window.onPointerDataPacket(ui.PointerDataPacket(data: [ _pointerData(PointerChange.add, const Offset(0.0, 0.0)), @@ -307,9 +317,9 @@ void main() { test('Finding no annotation is equivalent to specifying default cursor', () { final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[]; - MouseTrackerAnnotation annotation; + TestAnnotationTarget annotation; _setUpMouseTracker( - annotationFinder: (Offset position) => [if (annotation != null) annotation], + annotationFinder: (Offset position) => [if (annotation != null) annotation], logCursors: logCursors, ); @@ -324,7 +334,7 @@ void main() { logCursors.clear(); // Pointer moved to an annotation specified with the default cursor - annotation = const MouseTrackerAnnotation(cursor: SystemMouseCursors.basic); + annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.basic); ui.window.onPointerDataPacket(ui.PointerDataPacket(data: [ _pointerData(PointerChange.hover, const Offset(5.0, 0.0)), ])); @@ -351,14 +361,14 @@ void main() { test('Removing a pointer resets it back to the default cursor', () { final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[]; - MouseTrackerAnnotation annotation; + TestAnnotationTarget annotation; _setUpMouseTracker( - annotationFinder: (Offset position) => [if (annotation != null) annotation], + annotationFinder: (Offset position) => [if (annotation != null) annotation], logCursors: logCursors, ); // Pointer is added to the annotation, then removed - annotation = const MouseTrackerAnnotation(cursor: SystemMouseCursors.click); + annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.click); ui.window.onPointerDataPacket(ui.PointerDataPacket(data: [ _pointerData(PointerChange.add, const Offset(0.0, 0.0)), _pointerData(PointerChange.hover, const Offset(5.0, 0.0)), @@ -372,6 +382,7 @@ void main() { ui.window.onPointerDataPacket(ui.PointerDataPacket(data: [ _pointerData(PointerChange.add, const Offset(0.0, 0.0)), ])); + addTearDown(dispatchRemoveDevice); expect(logCursors, <_CursorUpdateDetails>[ _CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.basic.kind), @@ -384,9 +395,9 @@ void main() { _setUpMouseTracker( annotationFinder: (Offset position) sync* { if (position.dx > 200) { - yield const MouseTrackerAnnotation(cursor: SystemMouseCursors.forbidden); + yield const TestAnnotationTarget(cursor: SystemMouseCursors.forbidden); } else if (position.dx > 100) { - yield const MouseTrackerAnnotation(cursor: SystemMouseCursors.click); + yield const TestAnnotationTarget(cursor: SystemMouseCursors.click); } else {} }, logCursors: logCursors, @@ -397,6 +408,8 @@ void main() { _pointerData(PointerChange.add, const Offset(0.0, 0.0), device: 1), _pointerData(PointerChange.add, const Offset(0.0, 0.0), device: 2), ])); + addTearDown(() => dispatchRemoveDevice(1)); + addTearDown(() => dispatchRemoveDevice(2)); expect(logCursors, <_CursorUpdateDetails>[ _CursorUpdateDetails.activateSystemCursor(device: 1, kind: SystemMouseCursors.basic.kind), @@ -433,11 +446,6 @@ void main() { _CursorUpdateDetails.activateSystemCursor(device: 2, kind: SystemMouseCursors.forbidden.kind), ]); logCursors.clear(); - - // Remove - ui.window.onPointerDataPacket(ui.PointerDataPacket(data: [ - _pointerData(PointerChange.remove, const Offset(0.0, 0.0)), - ])); }); } @@ -492,42 +500,3 @@ class _CursorUpdateDetails extends MethodCall { return '_CursorUpdateDetails(method: $method, arguments: $arguments)'; } } - -class _TestGestureFlutterBinding extends BindingBase - with SchedulerBinding, ServicesBinding, GestureBinding, SemanticsBinding, RendererBinding { - @override - void initInstances() { - super.initInstances(); - postFrameCallbacks = []; - } - - SchedulerPhase _overridePhase; - @override - SchedulerPhase get schedulerPhase => _overridePhase ?? super.schedulerPhase; - - // Manually schedule a post-frame check. - // - // In real apps this is done by the renderer binding, but in tests we have to - // bypass the phase assertion of [MouseTracker.schedulePostFrameCheck]. - void scheduleMouseTrackerPostFrameCheck() { - final SchedulerPhase lastPhase = _overridePhase; - _overridePhase = SchedulerPhase.persistentCallbacks; - mouseTracker.schedulePostFrameCheck(); - _overridePhase = lastPhase; - } - - List postFrameCallbacks; - - // Proxy post-frame callbacks. - @override - void addPostFrameCallback(void Function(Duration) callback) { - postFrameCallbacks.add(callback); - } - - void flushPostFrameCallbacks(Duration duration) { - for (final void Function(Duration) callback in postFrameCallbacks) { - callback(duration); - } - postFrameCallbacks.clear(); - } -} diff --git a/packages/flutter/test/rendering/mouse_tracking_test.dart b/packages/flutter/test/rendering/mouse_tracking_test.dart index c7c8963d2a1..8a5105d125b 100644 --- a/packages/flutter/test/rendering/mouse_tracking_test.dart +++ b/packages/flutter/test/rendering/mouse_tracking_test.dart @@ -4,7 +4,6 @@ // @dart = 2.8 -import 'dart:collection' show LinkedHashMap; import 'dart:ui' as ui; import 'dart:ui' show PointerChange; @@ -12,86 +11,36 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; import 'package:vector_math/vector_math_64.dart' show Matrix4; import '../flutter_test_alternative.dart'; +import './mouse_tracking_test_utils.dart'; -typedef HandleEventCallback = void Function(PointerEvent event); - -class _TestGestureFlutterBinding extends BindingBase - with SchedulerBinding, ServicesBinding, GestureBinding, SemanticsBinding, RendererBinding { - @override - void initInstances() { - super.initInstances(); - postFrameCallbacks = []; - } - - SchedulerPhase _overridePhase; - @override - SchedulerPhase get schedulerPhase => _overridePhase ?? super.schedulerPhase; - - // Manually schedule a post-frame check. - // - // In real apps this is done by the renderer binding, but in tests we have to - // bypass the phase assertion of [MouseTracker.schedulePostFrameCheck]. - void scheduleMouseTrackerPostFrameCheck() { - final SchedulerPhase lastPhase = _overridePhase; - _overridePhase = SchedulerPhase.persistentCallbacks; - mouseTracker.schedulePostFrameCheck(); - _overridePhase = lastPhase; - } - - List postFrameCallbacks; - - // Proxy post-frame callbacks. - @override - void addPostFrameCallback(void Function(Duration) callback) { - postFrameCallbacks.add(callback); - } - - void flushPostFrameCallbacks(Duration duration) { - for (final void Function(Duration) callback in postFrameCallbacks) { - callback(duration); - } - postFrameCallbacks.clear(); - } -} - -_TestGestureFlutterBinding _binding = _TestGestureFlutterBinding(); +TestMouseTrackerFlutterBinding _binding = TestMouseTrackerFlutterBinding(); MouseTracker get _mouseTracker => RendererBinding.instance.mouseTracker; void _ensureTestGestureBinding() { - _binding ??= _TestGestureFlutterBinding(); + _binding ??= TestMouseTrackerFlutterBinding(); assert(GestureBinding.instance != null); } -@immutable -class AnnotationEntry { - AnnotationEntry(this.annotation, [Matrix4 transform]) - : transform = transform ?? Matrix4.identity(); - - final MouseTrackerAnnotation annotation; - final Matrix4 transform; -} - -typedef SimpleAnnotationFinder = Iterable Function(Offset offset); +typedef SimpleAnnotationFinder = Iterable Function(Offset offset); void main() { void _setUpMouseAnnotationFinder(SimpleAnnotationFinder annotationFinder) { - final MouseTracker mouseTracker = MouseTracker( - GestureBinding.instance.pointerRouter, - (Offset offset) => LinkedHashMap.fromEntries( - annotationFinder(offset).map( - (AnnotationEntry entry) => MapEntry( - entry.annotation, - entry.transform, - ), - ), - ), - ); - RendererBinding.instance.initMouseTracker(mouseTracker); + _binding.setHitTest((BoxHitTestResult result, Offset position) { + for (final TestAnnotationEntry entry in annotationFinder(position)) { + result.addWithRawTransform( + transform: entry.transform, + position: null, + hitTest: (BoxHitTestResult result, Offset position) { + result.add(entry); + return true; + }, + ); + } + return true; + }); } // Set up a trivial test environment that includes one annotation. @@ -99,8 +48,8 @@ void main() { // `logEvents`. // This annotation also contains a cursor with a value of `testCursor`. // The mouse tracker records the cursor requests it receives to `logCursors`. - MouseTrackerAnnotation _setUpWithOneAnnotation({List logEvents}) { - final MouseTrackerAnnotation annotation = MouseTrackerAnnotation( + TestAnnotationTarget _setUpWithOneAnnotation({List logEvents}) { + final TestAnnotationTarget oneAnnotation = TestAnnotationTarget( onEnter: (PointerEnterEvent event) { if (logEvents != null) logEvents.add(event); @@ -116,10 +65,16 @@ void main() { ); _setUpMouseAnnotationFinder( (Offset position) sync* { - yield AnnotationEntry(annotation); + yield TestAnnotationEntry(oneAnnotation); }, ); - return annotation; + return oneAnnotation; + } + + void dispatchRemoveDevice([int device = 0]) { + ui.window.onPointerDataPacket(ui.PointerDataPacket(data: [ + _pointerData(PointerChange.remove, const Offset(0.0, 0.0), device: device), + ])); } setUp(() { @@ -131,11 +86,10 @@ void main() { final MouseTrackerAnnotation annotation1 = MouseTrackerAnnotation( onEnter: (_) {}, onExit: (_) {}, - onHover: (_) {}, ); expect( annotation1.toString(), - equals('MouseTrackerAnnotation#${shortHash(annotation1)}(callbacks: [enter, hover, exit])'), + equals('MouseTrackerAnnotation#${shortHash(annotation1)}(callbacks: [enter, exit])'), ); const MouseTrackerAnnotation annotation2 = MouseTrackerAnnotation(); @@ -169,6 +123,7 @@ void main() { ui.window.onPointerDataPacket(ui.PointerDataPacket(data: [ _pointerData(PointerChange.add, const Offset(0.0, 0.0)), ])); + addTearDown(() => dispatchRemoveDevice()); expect(events, _equalToEventsOnCriticalFields([ const PointerEnterEvent(position: Offset(0.0, 0.0)), @@ -300,6 +255,7 @@ void main() { _pointerData(PointerChange.add, const Offset(0.0, 101.0)), _pointerData(PointerChange.down, const Offset(0.0, 101.0)), ])); + addTearDown(() => dispatchRemoveDevice()); expect(events, _equalToEventsOnCriticalFields([ // This Enter event is triggered by the [PointerAddedEvent] The // [PointerDownEvent] is ignored by [MouseTracker]. @@ -325,14 +281,14 @@ void main() { test('should correctly handle when the annotation appears or disappears on the pointer', () { bool isInHitRegion; final List events = []; - final MouseTrackerAnnotation annotation = MouseTrackerAnnotation( + final TestAnnotationTarget annotation = TestAnnotationTarget( onEnter: (PointerEnterEvent event) => events.add(event), onHover: (PointerHoverEvent event) => events.add(event), onExit: (PointerExitEvent event) => events.add(event), ); _setUpMouseAnnotationFinder((Offset position) sync* { if (isInHitRegion) { - yield AnnotationEntry(annotation, Matrix4.translationValues(10, 20, 0)); + yield TestAnnotationEntry(annotation, Matrix4.translationValues(10, 20, 0)); } }); @@ -342,6 +298,7 @@ void main() { ui.window.onPointerDataPacket(ui.PointerDataPacket(data: [ _pointerData(PointerChange.add, const Offset(0.0, 100.0)), ])); + addTearDown(() => dispatchRemoveDevice()); expect(events, _equalToEventsOnCriticalFields([ ])); expect(_mouseTracker.mouseIsConnected, isTrue); @@ -373,14 +330,14 @@ void main() { test('should correctly handle when the annotation moves in or out of the pointer', () { bool isInHitRegion; final List events = []; - final MouseTrackerAnnotation annotation = MouseTrackerAnnotation( + final TestAnnotationTarget annotation = TestAnnotationTarget( onEnter: (PointerEnterEvent event) => events.add(event), onHover: (PointerHoverEvent event) => events.add(event), onExit: (PointerExitEvent event) => events.add(event), ); _setUpMouseAnnotationFinder((Offset position) sync* { if (isInHitRegion) { - yield AnnotationEntry(annotation, Matrix4.translationValues(10, 20, 0)); + yield TestAnnotationEntry(annotation, Matrix4.translationValues(10, 20, 0)); } }); @@ -390,6 +347,7 @@ void main() { ui.window.onPointerDataPacket(ui.PointerDataPacket(data: [ _pointerData(PointerChange.add, const Offset(0.0, 100.0)), ])); + addTearDown(() => dispatchRemoveDevice()); events.clear(); // During a frame, the annotation moves into the pointer. @@ -422,14 +380,14 @@ void main() { test('should correctly handle when the pointer is added or removed on the annotation', () { bool isInHitRegion; final List events = []; - final MouseTrackerAnnotation annotation = MouseTrackerAnnotation( + final TestAnnotationTarget annotation = TestAnnotationTarget( onEnter: (PointerEnterEvent event) => events.add(event), onHover: (PointerHoverEvent event) => events.add(event), onExit: (PointerExitEvent event) => events.add(event), ); _setUpMouseAnnotationFinder((Offset position) sync* { if (isInHitRegion) { - yield AnnotationEntry(annotation, Matrix4.translationValues(10, 20, 0)); + yield TestAnnotationEntry(annotation, Matrix4.translationValues(10, 20, 0)); } }); @@ -460,14 +418,14 @@ void main() { test('should correctly handle when the pointer moves in or out of the annotation', () { bool isInHitRegion; final List events = []; - final MouseTrackerAnnotation annotation = MouseTrackerAnnotation( + final TestAnnotationTarget annotation = TestAnnotationTarget( onEnter: (PointerEnterEvent event) => events.add(event), onHover: (PointerHoverEvent event) => events.add(event), onExit: (PointerExitEvent event) => events.add(event), ); _setUpMouseAnnotationFinder((Offset position) sync* { if (isInHitRegion) { - yield AnnotationEntry(annotation, Matrix4.translationValues(10, 20, 0)); + yield TestAnnotationEntry(annotation, Matrix4.translationValues(10, 20, 0)); } }); @@ -475,6 +433,7 @@ void main() { ui.window.onPointerDataPacket(ui.PointerDataPacket(data: [ _pointerData(PointerChange.add, const Offset(200.0, 100.0)), ])); + addTearDown(() => dispatchRemoveDevice()); expect(_binding.postFrameCallbacks, hasLength(0)); events.clear(); @@ -518,17 +477,17 @@ void main() { test('should not flip out if not all mouse events are listened to', () { bool isInHitRegionOne = true; bool isInHitRegionTwo = false; - final MouseTrackerAnnotation annotation1 = MouseTrackerAnnotation( + final TestAnnotationTarget annotation1 = TestAnnotationTarget( onEnter: (PointerEnterEvent event) {} ); - final MouseTrackerAnnotation annotation2 = MouseTrackerAnnotation( + final TestAnnotationTarget annotation2 = TestAnnotationTarget( onExit: (PointerExitEvent event) {} ); _setUpMouseAnnotationFinder((Offset position) sync* { if (isInHitRegionOne) - yield AnnotationEntry(annotation1); + yield TestAnnotationEntry(annotation1); else if (isInHitRegionTwo) - yield AnnotationEntry(annotation2); + yield TestAnnotationEntry(annotation2); }); isInHitRegionOne = false; @@ -537,6 +496,7 @@ void main() { _pointerData(PointerChange.add, const Offset(0.0, 101.0)), _pointerData(PointerChange.hover, const Offset(1.0, 101.0)), ])); + addTearDown(() => dispatchRemoveDevice()); // Passes if no errors are thrown. }); @@ -553,12 +513,12 @@ void main() { bool isInB; final List logs = []; - final MouseTrackerAnnotation annotationA = MouseTrackerAnnotation( + final TestAnnotationTarget annotationA = TestAnnotationTarget( onEnter: (PointerEnterEvent event) => logs.add('enterA'), onExit: (PointerExitEvent event) => logs.add('exitA'), onHover: (PointerHoverEvent event) => logs.add('hoverA'), ); - final MouseTrackerAnnotation annotationB = MouseTrackerAnnotation( + final TestAnnotationTarget annotationB = TestAnnotationTarget( onEnter: (PointerEnterEvent event) => logs.add('enterB'), onExit: (PointerExitEvent event) => logs.add('exitB'), onHover: (PointerHoverEvent event) => logs.add('hoverB'), @@ -566,8 +526,8 @@ void main() { _setUpMouseAnnotationFinder((Offset position) sync* { // Children's annotations come before parents'. if (isInB) { - yield AnnotationEntry(annotationB); - yield AnnotationEntry(annotationA); + yield TestAnnotationEntry(annotationB); + yield TestAnnotationEntry(annotationA); } }); @@ -576,6 +536,7 @@ void main() { ui.window.onPointerDataPacket(ui.PointerDataPacket(data: [ _pointerData(PointerChange.add, const Offset(0.0, 1.0)), ])); + addTearDown(() => dispatchRemoveDevice()); expect(logs, []); // Moves into B within one frame. @@ -606,21 +567,21 @@ void main() { bool isInA; bool isInB; final List logs = []; - final MouseTrackerAnnotation annotationA = MouseTrackerAnnotation( + final TestAnnotationTarget annotationA = TestAnnotationTarget( onEnter: (PointerEnterEvent event) => logs.add('enterA'), onExit: (PointerExitEvent event) => logs.add('exitA'), onHover: (PointerHoverEvent event) => logs.add('hoverA'), ); - final MouseTrackerAnnotation annotationB = MouseTrackerAnnotation( + final TestAnnotationTarget annotationB = TestAnnotationTarget( onEnter: (PointerEnterEvent event) => logs.add('enterB'), onExit: (PointerExitEvent event) => logs.add('exitB'), onHover: (PointerHoverEvent event) => logs.add('hoverB'), ); _setUpMouseAnnotationFinder((Offset position) sync* { if (isInA) { - yield AnnotationEntry(annotationA); + yield TestAnnotationEntry(annotationA); } else if (isInB) { - yield AnnotationEntry(annotationB); + yield TestAnnotationEntry(annotationB); } }); @@ -630,6 +591,7 @@ void main() { ui.window.onPointerDataPacket(ui.PointerDataPacket(data: [ _pointerData(PointerChange.add, const Offset(0.0, 1.0)), ])); + addTearDown(() => dispatchRemoveDevice()); expect(logs, ['enterA']); logs.clear(); diff --git a/packages/flutter/test/rendering/mouse_tracking_test_utils.dart b/packages/flutter/test/rendering/mouse_tracking_test_utils.dart new file mode 100644 index 00000000000..650272c2fcf --- /dev/null +++ b/packages/flutter/test/rendering/mouse_tracking_test_utils.dart @@ -0,0 +1,107 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.8 + +import 'dart:ui' as ui; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; +import 'package:vector_math/vector_math_64.dart' show Matrix4; + +class _TestHitTester extends RenderBox { + _TestHitTester(this.hitTestOverride); + + final BoxHitTest hitTestOverride; + + @override + bool hitTest(BoxHitTestResult result, {ui.Offset position}) { + return hitTestOverride(result, position); + } +} + +// A binding used to test MouseTracker, allowing the test to override hit test +// searching. +class TestMouseTrackerFlutterBinding extends BindingBase + with SchedulerBinding, ServicesBinding, GestureBinding, SemanticsBinding, RendererBinding { + @override + void initInstances() { + super.initInstances(); + postFrameCallbacks = []; + } + + void setHitTest(BoxHitTest hitTest) { + renderView.child = _TestHitTester(hitTest); + } + + SchedulerPhase _overridePhase; + @override + SchedulerPhase get schedulerPhase => _overridePhase ?? super.schedulerPhase; + + // Manually schedule a post-frame check. + // + // In real apps this is done by the renderer binding, but in tests we have to + // bypass the phase assertion of [MouseTracker.schedulePostFrameCheck]. + void scheduleMouseTrackerPostFrameCheck() { + final SchedulerPhase lastPhase = _overridePhase; + _overridePhase = SchedulerPhase.persistentCallbacks; + addPostFrameCallback((_) { + mouseTracker.updateAllDevices(renderView.hitTestMouseTrackers); + }); + _overridePhase = lastPhase; + } + + List postFrameCallbacks; + + // Proxy post-frame callbacks. + @override + void addPostFrameCallback(void Function(Duration) callback) { + postFrameCallbacks.add(callback); + } + + void flushPostFrameCallbacks(Duration duration) { + for (final void Function(Duration) callback in postFrameCallbacks) { + callback(duration); + } + postFrameCallbacks.clear(); + } +} + +// An object that mocks the behavior of a render object with [MouseTrackerAnnotation]. +class TestAnnotationTarget with Diagnosticable implements MouseTrackerAnnotation, HitTestTarget { + const TestAnnotationTarget({this.onEnter, this.onHover, this.onExit, this.cursor = MouseCursor.defer}); + + @override + final PointerEnterEventListener onEnter; + + @override + final PointerHoverEventListener onHover; + + @override + final PointerExitEventListener onExit; + + @override + final MouseCursor cursor; + + @override + void handleEvent(PointerEvent event, HitTestEntry entry) { + if (event is PointerHoverEvent) + if (onHover != null) + onHover(event); + } +} + +// A hit test entry that can be assigned with a [TestAnnotationTarget] and an +// optional transform matrix. +class TestAnnotationEntry extends HitTestEntry { + TestAnnotationEntry(TestAnnotationTarget target, [Matrix4 transform]) + : transform = transform ?? Matrix4.identity(), super(target); + + @override + final Matrix4 transform; +} diff --git a/packages/flutter/test/rendering/platform_view_test.dart b/packages/flutter/test/rendering/platform_view_test.dart index 92268994fc3..4f6ce2b658b 100644 --- a/packages/flutter/test/rendering/platform_view_test.dart +++ b/packages/flutter/test/rendering/platform_view_test.dart @@ -4,12 +4,13 @@ // @dart = 2.8 +import 'dart:ui' as ui; + import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; -import '../gestures/gesture_tester.dart'; import '../services/fake_platform_views.dart'; import 'rendering_tester.dart'; @@ -73,16 +74,33 @@ void main() { semanticsHandle.dispose(); }); - testGesture('hover events are dispatched via PlatformViewController.dispatchPointerEvent', (GestureTester tester) { + test('mouse hover events are dispatched via PlatformViewController.dispatchPointerEvent', () { layout(platformViewRenderBox); pumpFrame(phase: EnginePhase.flushSemantics); - final TestPointer pointer = TestPointer(1, PointerDeviceKind.mouse); - tester.route(pointer.addPointer()); - tester.route(pointer.hover(const Offset(10, 10))); + ui.window.onPointerDataPacket(ui.PointerDataPacket(data: [ + _pointerData(ui.PointerChange.add, const Offset(0, 0)), + _pointerData(ui.PointerChange.hover, const Offset(10, 10)), + _pointerData(ui.PointerChange.remove, const Offset(10, 10)), + ])); expect(fakePlatformViewController.dispatchedPointerEvents, isNotEmpty); }); }, skip: isBrowser); // TODO(yjbanov): fails on Web with obscured stack trace: https://github.com/flutter/flutter/issues/42770 } + +ui.PointerData _pointerData( + ui.PointerChange change, + Offset logicalPosition, { + int device = 0, + PointerDeviceKind kind = PointerDeviceKind.mouse, +}) { + return ui.PointerData( + change: change, + physicalX: logicalPosition.dx * ui.window.devicePixelRatio, + physicalY: logicalPosition.dy * ui.window.devicePixelRatio, + kind: kind, + device: device, + ); +} diff --git a/packages/flutter/test/rendering/proxy_box_test.dart b/packages/flutter/test/rendering/proxy_box_test.dart index c182bd42fd4..3f1e91ec30a 100644 --- a/packages/flutter/test/rendering/proxy_box_test.dart +++ b/packages/flutter/test/rendering/proxy_box_test.dart @@ -4,7 +4,6 @@ // @dart = 2.8 -import 'dart:collection' show LinkedHashMap; import 'dart:typed_data'; import 'dart:ui' as ui show Gradient, Image, ImageFilter; @@ -491,10 +490,6 @@ void main() { }); test('RenderMouseRegion can change properties when detached', () { - renderer.initMouseTracker(MouseTracker( - renderer.pointerRouter, - (_) => {} as LinkedHashMap, - )); final RenderMouseRegion object = RenderMouseRegion(); object ..opaque = false diff --git a/packages/flutter/test/widgets/mouse_region_test.dart b/packages/flutter/test/widgets/mouse_region_test.dart index 9a1466c4ca3..783d926bc70 100644 --- a/packages/flutter/test/widgets/mouse_region_test.dart +++ b/packages/flutter/test/widgets/mouse_region_test.dart @@ -601,6 +601,8 @@ void main() { // Start outside, move inside, then move outside await gesture.moveTo(const Offset(150.0, 150.0)); await tester.pump(); + expect(logs, isEmpty); + logs.clear(); await gesture.moveTo(const Offset(50.0, 50.0)); await tester.pump(); await gesture.moveTo(const Offset(150.0, 150.0)); @@ -1106,14 +1108,15 @@ void main() { // Same as MouseRegion, but when opaque is null, use the default value. Widget mouseRegionWithOptionalOpaque({ void Function(PointerEnterEvent e) onEnter, + void Function(PointerHoverEvent e) onHover, void Function(PointerExitEvent e) onExit, Widget child, bool opaque, }) { if (opaque == null) { - return MouseRegion(onEnter: onEnter, onExit: onExit, child: child); + return MouseRegion(onEnter: onEnter, onHover: onHover, onExit: onExit, child: child); } - return MouseRegion(onEnter: onEnter, onExit: onExit, child: child, opaque: opaque); + return MouseRegion(onEnter: onEnter, onHover: onHover, onExit: onExit, child: child, opaque: opaque); } return Directionality( @@ -1122,6 +1125,7 @@ void main() { alignment: Alignment.topLeft, child: MouseRegion( onEnter: (PointerEnterEvent e) { addLog('enterA'); }, + onHover: (PointerHoverEvent e) { addLog('hoverA'); }, onExit: (PointerExitEvent e) { addLog('exitA'); }, child: SizedBox( width: 150, @@ -1135,6 +1139,7 @@ void main() { height: 80, child: MouseRegion( onEnter: (PointerEnterEvent e) { addLog('enterB'); }, + onHover: (PointerHoverEvent e) { addLog('hoverB'); }, onExit: (PointerExitEvent e) { addLog('exitB'); }, ), ), @@ -1146,6 +1151,7 @@ void main() { child: mouseRegionWithOptionalOpaque( opaque: opaqueC, onEnter: (PointerEnterEvent e) { addLog('enterC'); }, + onHover: (PointerHoverEvent e) { addLog('hoverC'); }, onExit: (PointerExitEvent e) { addLog('exitC'); }, ), ), @@ -1172,31 +1178,31 @@ void main() { // Move to the overlapping area. await gesture.moveTo(const Offset(75, 75)); await tester.pumpAndSettle(); - expect(logs, ['enterA', 'enterB', 'enterC']); + expect(logs, ['enterA', 'enterB', 'enterC', 'hoverA', 'hoverB', 'hoverC']); logs.clear(); // Move to the B only area. await gesture.moveTo(const Offset(25, 75)); await tester.pumpAndSettle(); - expect(logs, ['exitC']); + expect(logs, ['exitC', 'hoverA', 'hoverB']); logs.clear(); // Move back to the overlapping area. await gesture.moveTo(const Offset(75, 75)); await tester.pumpAndSettle(); - expect(logs, ['enterC']); + expect(logs, ['enterC', 'hoverA', 'hoverB', 'hoverC']); logs.clear(); // Move to the C only area. await gesture.moveTo(const Offset(125, 75)); await tester.pumpAndSettle(); - expect(logs, ['exitB']); + expect(logs, ['exitB', 'hoverA', 'hoverC']); logs.clear(); // Move back to the overlapping area. await gesture.moveTo(const Offset(75, 75)); await tester.pumpAndSettle(); - expect(logs, ['enterB']); + expect(logs, ['enterB', 'hoverA', 'hoverB', 'hoverC']); logs.clear(); // Move out. @@ -1220,31 +1226,31 @@ void main() { // Move to the overlapping area. await gesture.moveTo(const Offset(75, 75)); await tester.pumpAndSettle(); - expect(logs, ['enterA', 'enterC']); + expect(logs, ['enterA', 'enterC', 'hoverA', 'hoverC']); logs.clear(); // Move to the B only area. await gesture.moveTo(const Offset(25, 75)); await tester.pumpAndSettle(); - expect(logs, ['exitC', 'enterB']); + expect(logs, ['exitC', 'enterB', 'hoverA', 'hoverB']); logs.clear(); // Move back to the overlapping area. await gesture.moveTo(const Offset(75, 75)); await tester.pumpAndSettle(); - expect(logs, ['exitB', 'enterC']); + expect(logs, ['exitB', 'enterC', 'hoverA', 'hoverC']); logs.clear(); // Move to the C only area. await gesture.moveTo(const Offset(125, 75)); await tester.pumpAndSettle(); - expect(logs, []); + expect(logs, ['hoverA', 'hoverC']); logs.clear(); // Move back to the overlapping area. await gesture.moveTo(const Offset(75, 75)); await tester.pumpAndSettle(); - expect(logs, []); + expect(logs, ['hoverA', 'hoverC']); logs.clear(); // Move out. @@ -1268,7 +1274,7 @@ void main() { // Move to the overlapping area. await gesture.moveTo(const Offset(75, 75)); await tester.pumpAndSettle(); - expect(logs, ['enterA', 'enterC']); + expect(logs, ['enterA', 'enterC', 'hoverA', 'hoverC']); logs.clear(); // Move out.