Redo: Rewrite MouseTracker's tracking and notifying algorithm (#42486)
* Revert "Revert "Rewrite MouseTracker's tracking and notifying algorithm (#42031)" (#42478)"
This reverts commit eede792923ece2d76106c9e0742c45000be48834.
* Fix tests
diff --git a/packages/flutter/lib/src/foundation/diagnostics.dart b/packages/flutter/lib/src/foundation/diagnostics.dart
index b4c77ed..89c5769 100644
--- a/packages/flutter/lib/src/foundation/diagnostics.dart
+++ b/packages/flutter/lib/src/foundation/diagnostics.dart
@@ -2435,7 +2435,7 @@
///
/// See also:
///
-/// * [ObjectFlagSummary], which provides similar functionality but accepts
+/// * [ObjectFlagProperty], which provides similar functionality but accepts
/// only one flag, and is preferred if there is only one entry.
/// * [IterableProperty], which provides similar functionality describing
/// the values a collection of objects.
diff --git a/packages/flutter/lib/src/gestures/mouse_tracking.dart b/packages/flutter/lib/src/gestures/mouse_tracking.dart
index 9c35961..2060cb3 100644
--- a/packages/flutter/lib/src/gestures/mouse_tracking.dart
+++ b/packages/flutter/lib/src/gestures/mouse_tracking.dart
@@ -2,9 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'dart:collection' show LinkedHashSet;
import 'dart:ui';
-import 'package:flutter/foundation.dart' show ChangeNotifier, visibleForTesting;
+import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'events.dart';
@@ -48,49 +49,82 @@
@override
String toString() {
- final String none = (onEnter == null && onExit == null && onHover == null) ? ' <none>' : '';
- return '[$runtimeType${hashCode.toRadixString(16)}$none'
- '${onEnter == null ? '' : ' onEnter'}'
- '${onHover == null ? '' : ' onHover'}'
- '${onExit == null ? '' : ' onExit'}]';
+ final List<String> callbacks = <String>[];
+ if (onEnter != null)
+ callbacks.add('enter');
+ if (onHover != null)
+ callbacks.add('hover');
+ if (onExit != null)
+ callbacks.add('exit');
+ final String describeCallbacks = callbacks.isEmpty
+ ? '<none>'
+ : callbacks.join(' ');
+ return '${describeIdentity(this)}(callbacks: $describeCallbacks)';
}
}
-// Used internally by the MouseTracker for accounting for which annotation is
-// active on which devices inside of the MouseTracker.
-class _TrackedAnnotation {
- _TrackedAnnotation(this.annotation);
-
- final MouseTrackerAnnotation annotation;
-
- /// Tracks devices that are currently active for this annotation.
- ///
- /// If the mouse pointer corresponding to the integer device ID is
- /// present in the Set, then it is currently inside of the annotated layer.
- ///
- /// This is used to detect layers that used to have the mouse pointer inside
- /// them, but now no longer do (to facilitate exit notification).
- Set<int> activeDevices = <int>{};
-}
-
-/// Describes a function that finds an annotation given an offset in logical
-/// coordinates.
+/// Signature for searching for [MouseTrackerAnnotation]s at the given offset.
///
/// It is used by the [MouseTracker] to fetch annotations for the mouse
/// position.
typedef MouseDetectorAnnotationFinder = Iterable<MouseTrackerAnnotation> Function(Offset offset);
-/// Keeps state about which objects are interested in tracking mouse positions
-/// and notifies them when a mouse pointer enters, moves, or leaves an annotated
-/// region that they are interested in.
+// Various states of each connected mouse device.
+//
+// It is used by [MouseTracker] to compute which callbacks should be triggered
+// by each event.
+class _MouseState {
+ _MouseState({
+ @required PointerEvent mostRecentEvent,
+ }) : assert(mostRecentEvent != null),
+ _mostRecentEvent = mostRecentEvent;
+
+ // The list of annotations that contains this device during the last frame.
+ //
+ // It uses [LinkedHashSet] to keep the insertion order.
+ LinkedHashSet<MouseTrackerAnnotation> lastAnnotations = LinkedHashSet<MouseTrackerAnnotation>();
+
+ // The most recent mouse event observed from this device.
+ //
+ // The [mostRecentEvent] is never null.
+ PointerEvent get mostRecentEvent => _mostRecentEvent;
+ PointerEvent _mostRecentEvent;
+ set mostRecentEvent(PointerEvent value) {
+ assert(value != null);
+ assert(value.device == _mostRecentEvent.device);
+ _mostRecentEvent = value;
+ }
+
+ int get device => _mostRecentEvent.device;
+
+ @override
+ String toString() {
+ final String describeEvent = '${_mostRecentEvent.runtimeType}(device: ${_mostRecentEvent.device})';
+ final String describeAnnotations = '[list of ${lastAnnotations.length}]';
+ return '${describeIdentity(this)}(event: $describeEvent, annotations: $describeAnnotations)';
+ }
+}
+
+/// Maintains the relationship between mouse devices and
+/// [MouseTrackerAnnotation]s, and notifies interested callbacks of the changes
+/// thereof.
///
/// This class is a [ChangeNotifier] that notifies its listeners if the value of
/// [mouseIsConnected] changes.
///
-/// Owned by the [RendererBinding] class.
+/// An instance of [MouseTracker] is owned by the global singleton of
+/// [RendererBinding].
class MouseTracker extends ChangeNotifier {
/// Creates a mouse tracker 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.findAll] of the root layer.
+ ///
/// All of the parameters must not be null.
MouseTracker(this._router, this.annotationFinder)
: assert(_router != null),
@@ -104,60 +138,67 @@
_router.removeGlobalRoute(_handleEvent);
}
- // The pointer router that the mouse tracker listens to for events.
- final PointerRouter _router;
-
- /// Used to find annotations at a given logical coordinate.
+ /// Find annotations at a given offset in global logical coordinate space
+ /// in visual order from front to back.
+ ///
+ /// [MouseTracker] 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 collection of annotations that are currently being tracked. They may or
- // may not be active, depending on the value of _TrackedAnnotation.active.
- final Map<MouseTrackerAnnotation, _TrackedAnnotation> _trackedAnnotations = <MouseTrackerAnnotation, _TrackedAnnotation>{};
+ // The pointer router that the mouse tracker listens to, and receives new
+ // mouse events from.
+ final PointerRouter _router;
- /// Track an annotation so that if the mouse enters it, we send it events.
- ///
- /// This is typically called when the [AnnotatedRegion] containing this
- /// annotation has been added to the layer tree.
- void attachAnnotation(MouseTrackerAnnotation annotation) {
- _trackedAnnotations[annotation] = _TrackedAnnotation(annotation);
- // Schedule a check so that we test this new annotation to see if any mouse
- // is currently inside its region. It has to happen after the frame is
- // complete so that the annotation layer has been added before the check.
- if (mouseIsConnected) {
- _scheduleMousePositionCheck();
+ // Tracks the state of connected mouse devices.
+ //
+ // It is the source of truth for the list of connected mouse devices.
+ final Map<int, _MouseState> _mouseStates = <int, _MouseState>{};
+
+ // Returns the mouse state of a device. If it doesn't exist, create one using
+ // `mostRecentEvent`.
+ //
+ // The returned value is never null.
+ _MouseState _guaranteeMouseState(int device, PointerEvent mostRecentEvent) {
+ final _MouseState currentState = _mouseStates[device];
+ if (currentState == null) {
+ _addMouseDevice(device, mostRecentEvent);
+ }
+ final _MouseState result = currentState ?? _mouseStates[device];
+ assert(result != null);
+ return result;
+ }
+
+ // The collection of annotations that are currently being tracked.
+ // It is operated on by [attachAnnotation] and [detachAnnotation].
+ final Set<MouseTrackerAnnotation> _trackedAnnotations = <MouseTrackerAnnotation>{};
+ bool get _hasAttachedAnnotations => _trackedAnnotations.isNotEmpty;
+
+ void _addMouseDevice(int device, PointerEvent event) {
+ final bool wasConnected = mouseIsConnected;
+ assert(!_mouseStates.containsKey(device));
+ _mouseStates[device] = _MouseState(mostRecentEvent: event);
+ // Schedule a check to enter annotations that might contain this pointer.
+ _checkDeviceUpdates(device: device);
+ if (mouseIsConnected != wasConnected) {
+ notifyListeners();
}
}
- /// Stops tracking an annotation, indicating that it has been removed from the
- /// layer tree.
- ///
- /// An assertion error will be thrown if the associated layer is not removed
- /// and receives another mouse hit.
- void detachAnnotation(MouseTrackerAnnotation annotation) {
- final _TrackedAnnotation trackedAnnotation = _findAnnotation(annotation);
- for (int deviceId in trackedAnnotation.activeDevices) {
- if (annotation.onExit != null) {
- final PointerEvent event = _lastMouseEvent[deviceId] ?? _pendingRemovals[deviceId];
- assert(event != null);
- annotation.onExit(PointerExitEvent.fromMouseEvent(event));
- }
- }
- _trackedAnnotations.remove(annotation);
- }
-
- bool _scheduledPostFramePositionCheck = false;
- // Schedules a position check at the end of this frame for those annotations
- // that have been added.
- void _scheduleMousePositionCheck() {
- // If we're not tracking anything, then there is no point in registering a
- // frame callback or scheduling a frame. By definition there are no active
- // annotations that need exiting, either.
- if (_trackedAnnotations.isNotEmpty && !_scheduledPostFramePositionCheck) {
- _scheduledPostFramePositionCheck = true;
- SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
- _sendMouseNotifications(_lastMouseEvent.keys);
- _scheduledPostFramePositionCheck = false;
- });
+ void _removeMouseDevice(int device, PointerEvent event) {
+ final bool wasConnected = mouseIsConnected;
+ assert(_mouseStates.containsKey(device));
+ final _MouseState disconnectedMouseState = _mouseStates.remove(device);
+ disconnectedMouseState.mostRecentEvent = event;
+ // Schedule a check to exit annotations that used to contain this pointer.
+ _checkDeviceUpdates(
+ device: device,
+ disconnectedMouseState: disconnectedMouseState,
+ );
+ if (mouseIsConnected != wasConnected) {
+ notifyListeners();
}
}
@@ -166,40 +207,137 @@
if (event.kind != PointerDeviceKind.mouse) {
return;
}
- final int deviceId = event.device;
+ final int device = event.device;
if (event is PointerAddedEvent) {
- // If we are adding the device again, then we're not removing it anymore.
- _pendingRemovals.remove(deviceId);
- _addMouseEvent(deviceId, event);
- _sendMouseNotifications(<int>{deviceId});
- return;
- }
- if (event is PointerRemovedEvent) {
- _removeMouseEvent(deviceId, event);
- // If the mouse was removed, then we need to schedule one more check to
- // exit any annotations that were active.
- _sendMouseNotifications(<int>{deviceId});
- } else {
- if (event is PointerMoveEvent || event is PointerHoverEvent || event is PointerDownEvent) {
- final PointerEvent lastEvent = _lastMouseEvent[deviceId];
- _addMouseEvent(deviceId, event);
- if (lastEvent == null ||
- lastEvent is PointerAddedEvent || lastEvent.position != event.position) {
- // Only schedule a frame if we have our first event, or if the
- // location of the mouse has changed, and only if there are tracked annotations.
- _sendMouseNotifications(<int>{deviceId});
- }
+ _addMouseDevice(device, event);
+ } else if (event is PointerRemovedEvent) {
+ _removeMouseDevice(device, event);
+ } else if (event is PointerHoverEvent) {
+ final _MouseState mouseState = _guaranteeMouseState(device, event);
+ final PointerEvent previousEvent = mouseState.mostRecentEvent;
+ mouseState.mostRecentEvent = event;
+ if (previousEvent is PointerAddedEvent || previousEvent.position != event.position) {
+ // Only send notifications if we have our first event, or if the
+ // location of the mouse has changed
+ _checkDeviceUpdates(device: device);
}
}
}
- _TrackedAnnotation _findAnnotation(MouseTrackerAnnotation annotation) {
- final _TrackedAnnotation trackedAnnotation = _trackedAnnotations[annotation];
- assert(
- trackedAnnotation != null,
- 'Unable to find annotation $annotation in tracked annotations. '
- 'Check that attachAnnotation has been called for all annotated layers.');
- return trackedAnnotation;
+ bool _scheduledPostFramePositionCheck = false;
+ // Schedules a position check at the end of this frame.
+ // It is only called during a frame during which annotations have been added.
+ void _scheduleMousePositionCheck() {
+ // If we're not tracking anything, then there is no point in registering a
+ // frame callback or scheduling a frame. By definition there are no active
+ // annotations that need exiting, either.
+ if (!_scheduledPostFramePositionCheck) {
+ _scheduledPostFramePositionCheck = true;
+ SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
+ _checkAllDevicesUpdates();
+ _scheduledPostFramePositionCheck = false;
+ });
+ }
+ }
+
+ // Collect the latest states of the given mouse device `device`, and call
+ // interested callbacks.
+ //
+ // The enter or exit events are called for annotations that the pointer
+ // enters or leaves, while hover events are always called for each
+ // annotations that the pointer stays in, even if the pointer has not moved
+ // since the last call. Therefore it's caller's responsibility to check if
+ // the pointer has moved.
+ //
+ // If `disconnectedMouseState` is provided, this state will be used instead,
+ // but this mouse will be hovering no annotations.
+ void _checkDeviceUpdates({
+ int device,
+ _MouseState disconnectedMouseState,
+ }) {
+ final _MouseState mouseState = disconnectedMouseState ?? _mouseStates[device];
+ final bool thisDeviceIsConnected = mouseState != disconnectedMouseState;
+ assert(mouseState != null);
+
+ final LinkedHashSet<MouseTrackerAnnotation> nextAnnotations =
+ (_hasAttachedAnnotations && thisDeviceIsConnected)
+ ? LinkedHashSet<MouseTrackerAnnotation>.from(
+ annotationFinder(mouseState.mostRecentEvent.position)
+ )
+ : <MouseTrackerAnnotation>{};
+
+ _dispatchDeviceCallbacks(
+ currentState: mouseState,
+ nextAnnotations: nextAnnotations,
+ );
+
+ mouseState.lastAnnotations = nextAnnotations;
+ }
+
+ // Collect the latest states of all mouse devices, and call interested
+ // callbacks.
+ //
+ // For detailed behaviors, see [_checkDeviceUpdates].
+ void _checkAllDevicesUpdates() {
+ for (final int device in _mouseStates.keys) {
+ _checkDeviceUpdates(device: device);
+ }
+ }
+
+ // Dispatch callbacks related to a device after all necessary information
+ // has been collected.
+ //
+ // This function should not change the provided states, and should not access
+ // information that is not provided in parameters (hence being static).
+ static void _dispatchDeviceCallbacks({
+ @required LinkedHashSet<MouseTrackerAnnotation> nextAnnotations,
+ @required _MouseState currentState,
+ }) {
+ // Order is important for mouse event callbacks. The `findAnnotations`
+ // returns annotations in the visual order from front to back. We call
+ // it the "visual order", and the opposite one "reverse visual order".
+ // The algorithm here is explained in
+ // https://github.com/flutter/flutter/issues/41420
+
+ // The `nextAnnotations` is annotations that contains this device in the
+ // coming frame in visual order.
+ // Order is preserved with the help of [LinkedHashSet].
+
+ final PointerEvent mostRecentEvent = currentState.mostRecentEvent;
+ // The `lastAnnotations` is annotations that contains this device in the
+ // previous frame in visual order.
+ final LinkedHashSet<MouseTrackerAnnotation> lastAnnotations = currentState.lastAnnotations;
+
+ // Send exit events in visual order.
+ final Iterable<MouseTrackerAnnotation> exitingAnnotations =
+ lastAnnotations.difference(nextAnnotations);
+ for (final MouseTrackerAnnotation annotation in exitingAnnotations) {
+ if (annotation.onExit != null) {
+ annotation.onExit(PointerExitEvent.fromMouseEvent(mostRecentEvent));
+ }
+ }
+
+ // Send enter events in reverse visual order.
+ final Iterable<MouseTrackerAnnotation> enteringAnnotations =
+ nextAnnotations.difference(lastAnnotations).toList().reversed;
+ for (final MouseTrackerAnnotation annotation in enteringAnnotations) {
+ if (annotation.onEnter != null) {
+ annotation.onEnter(PointerEnterEvent.fromMouseEvent(mostRecentEvent));
+ }
+ }
+
+ // Send hover events in reverse visual order.
+ // For now the order between the hover events is designed this way for no
+ // solid reasons but to keep it aligned with enter events for simplicity.
+ if (mostRecentEvent is PointerHoverEvent) {
+ final Iterable<MouseTrackerAnnotation> hoveringAnnotations =
+ nextAnnotations.toList().reversed;
+ for (final MouseTrackerAnnotation annotation in hoveringAnnotations) {
+ if (annotation.onHover != null) {
+ annotation.onHover(mostRecentEvent);
+ }
+ }
+ }
}
/// Checks if the given [MouseTrackerAnnotation] is attached to this
@@ -209,127 +347,59 @@
/// MouseTracker. Do not call in other contexts.
@visibleForTesting
bool isAnnotationAttached(MouseTrackerAnnotation annotation) {
- return _trackedAnnotations.containsKey(annotation);
+ return _trackedAnnotations.contains(annotation);
}
- // Tells interested objects that a mouse has entered, exited, or moved, given
- // a callback to fetch the [MouseTrackerAnnotation] associated with a global
- // offset.
- //
- // This is called from a post-frame callback when the layer tree has been
- // updated, right after rendering the frame.
- void _sendMouseNotifications(Iterable<int> deviceIds) {
- if (_trackedAnnotations.isEmpty) {
- return;
- }
-
- void exitAnnotation(_TrackedAnnotation trackedAnnotation, int deviceId) {
- if (trackedAnnotation.annotation?.onExit != null && trackedAnnotation.activeDevices.contains(deviceId)) {
- final PointerEvent event = _lastMouseEvent[deviceId] ?? _pendingRemovals[deviceId];
- assert(event != null);
- trackedAnnotation.annotation.onExit(PointerExitEvent.fromMouseEvent(event));
- }
- trackedAnnotation.activeDevices.remove(deviceId);
- }
-
- void exitAllDevices(_TrackedAnnotation trackedAnnotation) {
- if (trackedAnnotation.activeDevices.isNotEmpty) {
- final Set<int> deviceIds = trackedAnnotation.activeDevices.toSet();
- for (int deviceId in deviceIds) {
- exitAnnotation(trackedAnnotation, deviceId);
- }
- }
- }
-
- try {
- // This indicates that all mouse pointers were removed, or none have been
- // connected yet. If no mouse is connected, then we want to make sure that
- // all active annotations are exited.
- if (!mouseIsConnected) {
- _trackedAnnotations.values.forEach(exitAllDevices);
- return;
- }
-
- for (int deviceId in deviceIds) {
- final PointerEvent lastEvent = _lastMouseEvent[deviceId];
- assert(lastEvent != null);
- final Iterable<MouseTrackerAnnotation> hits = annotationFinder(lastEvent.position);
-
- // No annotations were found at this position for this deviceId, so send an
- // exit to all active tracked annotations, since none of them were hit.
- if (hits.isEmpty) {
- // Send an exit to all tracked animations tracking this deviceId.
- for (_TrackedAnnotation trackedAnnotation in _trackedAnnotations.values) {
- exitAnnotation(trackedAnnotation, deviceId);
- }
- continue;
- }
-
- final Set<_TrackedAnnotation> hitAnnotations = hits.map<_TrackedAnnotation>((MouseTrackerAnnotation hit) => _findAnnotation(hit)).toSet();
- for (_TrackedAnnotation hitAnnotation in hitAnnotations) {
- if (!hitAnnotation.activeDevices.contains(deviceId)) {
- // A tracked annotation that just became active and needs to have an enter
- // event sent to it.
- hitAnnotation.activeDevices.add(deviceId);
- if (hitAnnotation.annotation?.onEnter != null) {
- hitAnnotation.annotation.onEnter(PointerEnterEvent.fromMouseEvent(lastEvent));
- }
- }
- if (hitAnnotation.annotation?.onHover != null && lastEvent is PointerHoverEvent) {
- hitAnnotation.annotation.onHover(lastEvent);
- }
-
- // Tell any tracked annotations that weren't hit that they are no longer
- // active.
- for (_TrackedAnnotation trackedAnnotation in _trackedAnnotations.values) {
- if (hitAnnotations.contains(trackedAnnotation)) {
- continue;
- }
- if (trackedAnnotation.activeDevices.contains(deviceId)) {
- if (trackedAnnotation.annotation?.onExit != null) {
- trackedAnnotation.annotation.onExit(PointerExitEvent.fromMouseEvent(lastEvent));
- }
- trackedAnnotation.activeDevices.remove(deviceId);
- }
- }
- }
- }
- } finally {
- _pendingRemovals.clear();
- }
- }
-
- void _addMouseEvent(int deviceId, PointerEvent event) {
- final bool wasConnected = mouseIsConnected;
- if (event is PointerAddedEvent) {
- // If we are adding the device again, then we're not removing it anymore.
- _pendingRemovals.remove(deviceId);
- }
- _lastMouseEvent[deviceId] = event;
- if (mouseIsConnected != wasConnected) {
- notifyListeners();
- }
- }
-
- void _removeMouseEvent(int deviceId, PointerEvent event) {
- final bool wasConnected = mouseIsConnected;
- assert(event is PointerRemovedEvent);
- _pendingRemovals[deviceId] = event;
- _lastMouseEvent.remove(deviceId);
- if (mouseIsConnected != wasConnected) {
- notifyListeners();
- }
- }
-
- // A list of device IDs that should be removed and notified when scheduling a
- // mouse position check.
- final Map<int, PointerRemovedEvent> _pendingRemovals = <int, PointerRemovedEvent>{};
-
- /// The most recent mouse event observed for each mouse device ID observed.
- ///
- /// May be null if no mouse is connected, or hasn't produced an event yet.
- final Map<int, PointerEvent> _lastMouseEvent = <int, PointerEvent>{};
-
/// Whether or not a mouse is connected and has produced events.
- bool get mouseIsConnected => _lastMouseEvent.isNotEmpty;
+ bool get mouseIsConnected => _mouseStates.isNotEmpty;
+
+ /// Notify [MouseTracker] that a new mouse tracker annotation has started to
+ /// take effect.
+ ///
+ /// This should be called as soon as the layer that owns this annotation is
+ /// added to the layer tree.
+ ///
+ /// This triggers [MouseTracker] to schedule a mouse position check during the
+ /// post frame to see if this new annotation might trigger enter events.
+ ///
+ /// The [MouseTracker] also uses this to track the number of attached
+ /// annotations, and will skip mouse position checks if there is no
+ /// annotations attached.
+ void attachAnnotation(MouseTrackerAnnotation annotation) {
+ // Schedule a check so that we test this new annotation to see if any mouse
+ // is currently inside its region. It has to happen after the frame is
+ // complete so that the annotation layer has been added before the check.
+ _trackedAnnotations.add(annotation);
+ if (mouseIsConnected) {
+ _scheduleMousePositionCheck();
+ }
+ }
+
+
+ /// Notify [MouseTracker] that a mouse tracker annotation that was previously
+ /// attached has stopped taking effect.
+ ///
+ /// This should be called as soon as the layer that owns this annotation is
+ /// removed from the layer tree. An assertion error will be thrown if the
+ /// associated layer is not removed and receives another mouse hit.
+ ///
+ /// This triggers [MouseTracker] to perform a mouse position check immediately
+ /// to see if this annotation removal triggers any exit events.
+ ///
+ /// The [MouseTracker] also uses this to track the number of attached
+ /// annotations, and will skip mouse position checks if there is no
+ /// annotations attached.
+ void detachAnnotation(MouseTrackerAnnotation annotation) {
+ _mouseStates.forEach((int device, _MouseState mouseState) {
+ if (mouseState.lastAnnotations.contains(annotation)) {
+ if (annotation.onExit != null) {
+ final PointerEvent event = mouseState.mostRecentEvent;
+ assert(event != null);
+ annotation.onExit(PointerExitEvent.fromMouseEvent(event));
+ }
+ mouseState.lastAnnotations.remove(annotation);
+ }
+ });
+ _trackedAnnotations.remove(annotation);
+ }
}
diff --git a/packages/flutter/test/gestures/mouse_tracking_test.dart b/packages/flutter/test/gestures/mouse_tracking_test.dart
index 6987010..8cc3629 100644
--- a/packages/flutter/test/gestures/mouse_tracking_test.dart
+++ b/packages/flutter/test/gestures/mouse_tracking_test.dart
@@ -16,211 +16,579 @@
typedef HandleEventCallback = void Function(PointerEvent event);
-class TestGestureFlutterBinding extends BindingBase with ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, RendererBinding {
- HandleEventCallback callback;
-
+class _TestGestureFlutterBinding extends BindingBase
+ with ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, RendererBinding {
@override
- void handleEvent(PointerEvent event, HitTestEntry entry) {
- super.handleEvent(event, entry);
- if (callback != null) {
- callback(event);
+ void initInstances() {
+ super.initInstances();
+ postFrameCallbacks = <void Function(Duration)>[];
+ }
+
+ List<void Function(Duration)> 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();
+_TestGestureFlutterBinding _binding = _TestGestureFlutterBinding();
+MouseTracker get _mouseTracker => RendererBinding.instance.mouseTracker;
-void ensureTestGestureBinding() {
- _binding ??= TestGestureFlutterBinding();
+void _ensureTestGestureBinding() {
+ _binding ??= _TestGestureFlutterBinding();
assert(GestureBinding.instance != null);
}
void main() {
- setUp(ensureTestGestureBinding);
+ void _setUpMouseAnnotationFinder(MouseDetectorAnnotationFinder annotationFinder) {
+ final MouseTracker mouseTracker = MouseTracker(
+ GestureBinding.instance.pointerRouter,
+ annotationFinder,
+ );
+ RendererBinding.instance.initMouseTracker(mouseTracker);
+ }
- final List<PointerEvent> events = <PointerEvent>[];
- final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
- onEnter: (PointerEnterEvent event) => events.add(event),
- onHover: (PointerHoverEvent event) => events.add(event),
- onExit: (PointerExitEvent event) => events.add(event),
- );
- // Only respond to some mouse events.
- final MouseTrackerAnnotation partialAnnotation = MouseTrackerAnnotation(
- onEnter: (PointerEnterEvent event) => events.add(event),
- onHover: (PointerHoverEvent event) => events.add(event),
- );
- bool isInHitRegionOne;
- bool isInHitRegionTwo;
-
- void clear() {
- events.clear();
+ // Set up a trivial test environment that includes one annotation, which adds
+ // the enter, hover, and exit events it received to [logEvents].
+ MouseTrackerAnnotation _setUpWithOneAnnotation({List<PointerEvent> logEvents}) {
+ final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
+ onEnter: (PointerEnterEvent event) => logEvents.add(event),
+ onHover: (PointerHoverEvent event) => logEvents.add(event),
+ onExit: (PointerExitEvent event) => logEvents.add(event),
+ );
+ _setUpMouseAnnotationFinder(
+ (Offset position) sync* {
+ yield annotation;
+ },
+ );
+ _mouseTracker.attachAnnotation(annotation);
+ return annotation;
}
setUp(() {
- clear();
- isInHitRegionOne = true;
- isInHitRegionTwo = false;
- RendererBinding.instance.initMouseTracker(
- MouseTracker(
- GestureBinding.instance.pointerRouter,
- (Offset position) sync* {
- if (isInHitRegionOne)
- yield annotation;
- else if (isInHitRegionTwo) {
- yield partialAnnotation;
- }
- },
- ),
- );
+ _ensureTestGestureBinding();
+ _binding.postFrameCallbacks.clear();
PointerEventConverter.clearPointers();
});
- test('receives and processes mouse hover events', () {
- final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[
- // Will implicitly also add a PointerAdded event.
- _pointerData(PointerChange.hover, const Offset(0.0, 0.0)),
- ]);
- final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[
- _pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
- ]);
- final ui.PointerDataPacket packet3 = ui.PointerDataPacket(data: <ui.PointerData>[
- _pointerData(PointerChange.remove, const Offset(1.0, 201.0)),
- ]);
- final ui.PointerDataPacket packet4 = ui.PointerDataPacket(data: <ui.PointerData>[
- _pointerData(PointerChange.hover, const Offset(1.0, 301.0)),
- ]);
- final ui.PointerDataPacket packet5 = ui.PointerDataPacket(data: <ui.PointerData>[
- _pointerData(PointerChange.hover, const Offset(1.0, 401.0), device: 1),
- ]);
- RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
- isInHitRegionOne = true;
- ui.window.onPointerDataPacket(packet1);
- expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
- const PointerEnterEvent(position: Offset(0.0, 0.0)),
- const PointerHoverEvent(position: Offset(0.0, 0.0)),
- ]));
- clear();
+ test('MouseTrackerAnnotation has correct toString', () {
+ final MouseTrackerAnnotation annotation1 = MouseTrackerAnnotation(
+ onEnter: (_) {},
+ onExit: (_) {},
+ onHover: (_) {},
+ );
+ expect(
+ annotation1.toString(),
+ equals('MouseTrackerAnnotation#${shortHash(annotation1)}(callbacks: enter hover exit)'),
+ );
- ui.window.onPointerDataPacket(packet2);
+ const MouseTrackerAnnotation annotation2 = MouseTrackerAnnotation();
+ expect(
+ annotation2.toString(),
+ equals('MouseTrackerAnnotation#${shortHash(annotation2)}(callbacks: <none>)'),
+ );
+ });
+
+ test('should detect enter, hover, and exit from Added, Hover, and Removed events', () {
+ final List<PointerEvent> events = <PointerEvent>[];
+ _setUpWithOneAnnotation(logEvents: events);
+
+ final List<bool> listenerLogs = <bool>[];
+ _mouseTracker.addListener(() {
+ listenerLogs.add(_mouseTracker.mouseIsConnected);
+ });
+
+ expect(_mouseTracker.mouseIsConnected, isFalse);
+
+ // Enter
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.hover, const Offset(1.0, 0.0)),
+ ]));
+ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+ const PointerEnterEvent(position: Offset(1.0, 0.0)),
+ const PointerHoverEvent(position: Offset(1.0, 0.0)),
+ ]));
+ expect(listenerLogs, <bool>[true]);
+ events.clear();
+ listenerLogs.clear();
+
+ // Hover
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
+ ]));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerHoverEvent(position: Offset(1.0, 101.0)),
]));
- clear();
+ expect(_mouseTracker.mouseIsConnected, isTrue);
+ expect(listenerLogs, <bool>[]);
+ events.clear();
- ui.window.onPointerDataPacket(packet3);
+ // Remove
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.remove, const Offset(1.0, 201.0)),
+ ]));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerHoverEvent(position: Offset(1.0, 201.0)),
const PointerExitEvent(position: Offset(1.0, 201.0)),
]));
+ expect(listenerLogs, <bool>[false]);
+ events.clear();
+ listenerLogs.clear();
- clear();
- ui.window.onPointerDataPacket(packet4);
+ // Add again
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.hover, const Offset(1.0, 301.0)),
+ ]));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerEnterEvent(position: Offset(1.0, 301.0)),
const PointerHoverEvent(position: Offset(1.0, 301.0)),
]));
+ expect(listenerLogs, <bool>[true]);
+ events.clear();
+ listenerLogs.clear();
+ });
- // add in a second mouse simultaneously.
- clear();
- ui.window.onPointerDataPacket(packet5);
+ test('should correctly handle multiple devices', () {
+ final List<PointerEvent> events = <PointerEvent>[];
+ _setUpWithOneAnnotation(logEvents: events);
+
+ expect(_mouseTracker.mouseIsConnected, isFalse);
+
+ // First mouse
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.hover, const Offset(0.0, 1.0)),
+ ]));
+ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+ const PointerEnterEvent(position: Offset(0.0, 1.0)),
+ const PointerHoverEvent(position: Offset(0.0, 1.0)),
+ ]));
+ expect(_mouseTracker.mouseIsConnected, isTrue);
+ events.clear();
+
+ // Second mouse
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.hover, const Offset(1.0, 401.0), device: 1),
+ ]));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerEnterEvent(position: Offset(1.0, 401.0), device: 1),
const PointerHoverEvent(position: Offset(1.0, 401.0), device: 1),
]));
+ expect(_mouseTracker.mouseIsConnected, isTrue);
+ events.clear();
+
+ // First mouse hover
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.hover, const Offset(0.0, 101.0)),
+ ]));
+ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+ const PointerHoverEvent(position: Offset(0.0, 101.0)),
+ ]));
+ expect(_mouseTracker.mouseIsConnected, isTrue);
+ events.clear();
+
+ // Second mouse hover
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.hover, const Offset(1.0, 501.0), device: 1),
+ ]));
+ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+ const PointerHoverEvent(position: Offset(1.0, 501.0), device: 1),
+ ]));
+ expect(_mouseTracker.mouseIsConnected, isTrue);
+ events.clear();
+
+ // First mouse remove
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.remove, const Offset(0.0, 101.0)),
+ ]));
+ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+ const PointerExitEvent(position: Offset(0.0, 101.0)),
+ ]));
+ expect(_mouseTracker.mouseIsConnected, isTrue);
+ events.clear();
+
+ // Second mouse hover
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.hover, const Offset(1.0, 601.0), device: 1),
+ ]));
+ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+ const PointerHoverEvent(position: Offset(1.0, 601.0), device: 1),
+ ]));
+ expect(_mouseTracker.mouseIsConnected, isTrue);
+ events.clear();
+
+ // Second mouse remove
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.remove, const Offset(1.0, 601.0), device: 1),
+ ]));
+ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+ const PointerExitEvent(position: Offset(1.0, 601.0), device: 1),
+ ]));
+ expect(_mouseTracker.mouseIsConnected, isFalse);
+ events.clear();
});
- test('detects exit when annotated layer no longer hit', () {
- final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[
- _pointerData(PointerChange.hover, const Offset(0.0, 0.0)),
- _pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
- ]);
- final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[
- _pointerData(PointerChange.hover, const Offset(1.0, 201.0)),
- ]);
- isInHitRegionOne = true;
- RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
+ test('should handle detaching during the callback of exiting', () {
+ bool isInHitRegion;
+ final List<PointerEvent> events = <PointerEvent>[];
+ final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
+ 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 annotation;
+ }
+ });
- ui.window.onPointerDataPacket(packet1);
+ isInHitRegion = true;
+ _mouseTracker.attachAnnotation(annotation);
- expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
- const PointerEnterEvent(position: Offset(0.0, 0.0)),
- const PointerHoverEvent(position: Offset(0.0, 0.0)),
- const PointerHoverEvent(position: Offset(1.0, 101.0)),
+ // Enter
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.hover, const Offset(1.0, 0.0)),
]));
- // Simulate layer going away by detaching it.
- clear();
- isInHitRegionOne = false;
-
- ui.window.onPointerDataPacket(packet2);
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
- const PointerExitEvent(position: Offset(1.0, 201.0)),
+ const PointerEnterEvent(position: Offset(1.0, 0.0)),
+ const PointerHoverEvent(position: Offset(1.0, 0.0)),
]));
+ expect(_mouseTracker.mouseIsConnected, isTrue);
+ events.clear();
- // Actually detach annotation. Shouldn't receive hit.
- RendererBinding.instance.mouseTracker.detachAnnotation(annotation);
- clear();
- isInHitRegionOne = false;
+ // Remove
+ _mouseTracker.addListener(() {
+ if (!_mouseTracker.mouseIsConnected) {
+ _mouseTracker.detachAnnotation(annotation);
+ isInHitRegion = false;
+ }
+ });
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.remove, const Offset(1.0, 0.0)),
+ ]));
+ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+ const PointerExitEvent(position: Offset(1.0, 0.0)),
+ ]));
+ expect(_mouseTracker.mouseIsConnected, isFalse);
+ events.clear();
+ });
- ui.window.onPointerDataPacket(packet2);
+ test('should not handle non-hover events', () {
+ final List<PointerEvent> events = <PointerEvent>[];
+ _setUpWithOneAnnotation(logEvents: events);
+
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.down, const Offset(0.0, 101.0)),
+ ]));
+ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+ // This Enter event is triggered by the [PointerAddedEvent] that was
+ // synthesized during the event normalization of pointer event converter.
+ // The [PointerDownEvent] is ignored by [MouseTracker].
+ const PointerEnterEvent(position: Offset(0.0, 101.0)),
+ ]));
+ events.clear();
+
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.move, const Offset(0.0, 201.0)),
+ ]));
+ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+ ]));
+ events.clear();
+
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.up, const Offset(0.0, 301.0)),
+ ]));
+ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+ ]));
+ events.clear();
+ });
+
+ test('should detect enter or exit when annotations are attached or detached on the pointer', () {
+ bool isInHitRegion;
+ final List<PointerEvent> events = <PointerEvent>[];
+ final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
+ 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 annotation;
+ }
+ });
+
+ isInHitRegion = false;
+
+ // Connect a mouse when there is no annotation
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.add, const Offset(0.0, 100.0)),
+ ]));
+ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+ ]));
+ expect(_mouseTracker.mouseIsConnected, isTrue);
+ events.clear();
+
+ // Attach an annotation
+ isInHitRegion = true;
+ _mouseTracker.attachAnnotation(annotation);
+ // No callbacks are triggered immediately
+ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+ ]));
+ expect(_binding.postFrameCallbacks, hasLength(1));
+
+ _binding.flushPostFrameCallbacks(Duration.zero);
+ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+ const PointerEnterEvent(position: Offset(0.0, 100.0)),
+ ]));
+ events.clear();
+
+ // Detach the annotation
+ isInHitRegion = false;
+ _mouseTracker.detachAnnotation(annotation);
+ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+ const PointerExitEvent(position: Offset(0.0, 100.0)),
+ ]));
+ expect(_binding.postFrameCallbacks, hasLength(0));
+ });
+
+ test('should correctly stay quiet when annotations are attached or detached not on the pointer', () {
+ final List<PointerEvent> events = <PointerEvent>[];
+ final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
+ onEnter: (PointerEnterEvent event) => events.add(event),
+ onHover: (PointerHoverEvent event) => events.add(event),
+ onExit: (PointerExitEvent event) => events.add(event),
+ );
+ _setUpMouseAnnotationFinder((Offset position) sync* {
+ // This annotation is never in the region
+ });
+
+ // Connect a mouse when there is no annotation
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.add, const Offset(0.0, 100.0)),
+ ]));
+ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+ ]));
+ expect(_mouseTracker.mouseIsConnected, isTrue);
+ events.clear();
+
+ // Attach an annotation out of region
+ _mouseTracker.attachAnnotation(annotation);
+ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+ ]));
+ expect(_binding.postFrameCallbacks, hasLength(1));
+
+ _binding.flushPostFrameCallbacks(Duration.zero);
+ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+ ]));
+ events.clear();
+
+ // Detach the annotation
+ _mouseTracker.detachAnnotation(annotation);
+ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+ ]));
+ expect(_binding.postFrameCallbacks, hasLength(0));
+
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.remove, const Offset(0.0, 100.0)),
+ ]));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
]));
});
- test("don't flip out if not all mouse events are listened to", () {
+ test('should not flip out if not all mouse events are listened to', () {
+ bool isInHitRegionOne = true;
+ bool isInHitRegionTwo = false;
+ final MouseTrackerAnnotation annotation1 = MouseTrackerAnnotation(
+ onEnter: (PointerEnterEvent event) {}
+ );
+ final MouseTrackerAnnotation annotation2 = MouseTrackerAnnotation(
+ onExit: (PointerExitEvent event) {}
+ );
+ _setUpMouseAnnotationFinder((Offset position) sync* {
+ if (isInHitRegionOne)
+ yield annotation1;
+ else if (isInHitRegionTwo)
+ yield annotation2;
+ });
+
final ui.PointerDataPacket packet = ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
]);
isInHitRegionOne = false;
isInHitRegionTwo = true;
- RendererBinding.instance.mouseTracker.attachAnnotation(partialAnnotation);
+ _mouseTracker.attachAnnotation(annotation2);
ui.window.onPointerDataPacket(packet);
- RendererBinding.instance.mouseTracker.detachAnnotation(partialAnnotation);
+ _mouseTracker.detachAnnotation(annotation2);
isInHitRegionTwo = false;
// Passes if no errors are thrown
});
- test('detects exit when mouse goes away', () {
- final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[
- _pointerData(PointerChange.hover, const Offset(0.0, 0.0)),
- _pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
- ]);
- final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[
- _pointerData(PointerChange.remove, const Offset(1.0, 201.0)),
- ]);
- isInHitRegionOne = true;
- RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
- ui.window.onPointerDataPacket(packet1);
- ui.window.onPointerDataPacket(packet2);
- expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
- const PointerEnterEvent(position: Offset(0.0, 0.0)),
- const PointerHoverEvent(position: Offset(0.0, 0.0)),
- const PointerHoverEvent(position: Offset(1.0, 101.0)),
- const PointerHoverEvent(position: Offset(1.0, 201.0)),
- const PointerExitEvent(position: Offset(1.0, 201.0)),
+ test('should not call annotationFinder when no annotations are attached', () {
+ final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
+ onEnter: (PointerEnterEvent event) {},
+ );
+ int finderCalled = 0;
+ _setUpMouseAnnotationFinder((Offset position) sync* {
+ finderCalled++;
+ // This annotation is never in the region
+ });
+
+ // When no annotations are attached, hovering should not call finder.
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.hover, const Offset(0.0, 101.0)),
]));
+ expect(finderCalled, 0);
+
+ // Attaching should call finder during the post frame.
+ _mouseTracker.attachAnnotation(annotation);
+ expect(finderCalled, 0);
+
+ _binding.flushPostFrameCallbacks(Duration.zero);
+ expect(finderCalled, 1);
+ finderCalled = 0;
+
+ // When annotations are attached, hovering should call finder.
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.hover, const Offset(0.0, 201.0)),
+ ]));
+ expect(finderCalled, 1);
+ finderCalled = 0;
+
+ // Detaching an annotation should not call finder (because only history
+ // records are needed).
+ _mouseTracker.detachAnnotation(annotation);
+ expect(finderCalled, 0);
+
+ // When all annotations are detached, hovering should not call finder.
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.hover, const Offset(0.0, 201.0)),
+ ]));
+ expect(finderCalled, 0);
});
- test('handles mouse down and move', () {
- final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[
- _pointerData(PointerChange.hover, const Offset(0.0, 0.0)),
- _pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
- ]);
- final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[
- _pointerData(PointerChange.down, const Offset(1.0, 101.0)),
- _pointerData(PointerChange.move, const Offset(1.0, 201.0)),
- ]);
- isInHitRegionOne = true;
- RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
- ui.window.onPointerDataPacket(packet1);
- ui.window.onPointerDataPacket(packet2);
- expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
- const PointerEnterEvent(position: Offset(0.0, 0.0), delta: Offset(0.0, 0.0)),
- const PointerHoverEvent(position: Offset(0.0, 0.0), delta: Offset(0.0, 0.0)),
- const PointerHoverEvent(position: Offset(1.0, 101.0), delta: Offset(1.0, 101.0)),
+ test('should trigger callbacks between parents and children in correct order', () {
+ // This test simulates the scenario of a layer being the child of another.
+ //
+ // ———————————
+ // |A |
+ // | —————— |
+ // | |B | |
+ // | —————— |
+ // ———————————
+
+ bool isInB;
+ final List<String> logs = <String>[];
+ final MouseTrackerAnnotation annotationA = MouseTrackerAnnotation(
+ onEnter: (PointerEnterEvent event) => logs.add('enterA'),
+ onExit: (PointerExitEvent event) => logs.add('exitA'),
+ onHover: (PointerHoverEvent event) => logs.add('hoverA'),
+ );
+ final MouseTrackerAnnotation annotationB = MouseTrackerAnnotation(
+ onEnter: (PointerEnterEvent event) => logs.add('enterB'),
+ onExit: (PointerExitEvent event) => logs.add('exitB'),
+ onHover: (PointerHoverEvent event) => logs.add('hoverB'),
+ );
+ _setUpMouseAnnotationFinder((Offset position) sync* {
+ // Children's annotations come before parents'
+ if (isInB) {
+ yield annotationB;
+ yield annotationA;
+ }
+ });
+ _mouseTracker.attachAnnotation(annotationA);
+ _mouseTracker.attachAnnotation(annotationB);
+
+ // Starts out of A
+ isInB = false;
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.hover, const Offset(0.0, 1.0)),
]));
+ expect(logs, <String>[]);
+
+ // Moves into B within one frame
+ isInB = true;
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.hover, const Offset(0.0, 10.0)),
+ ]));
+ expect(logs, <String>['enterA', 'enterB', 'hoverA', 'hoverB']);
+ logs.clear();
+
+ // Moves out of A within one frame
+ isInB = false;
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.hover, const Offset(0.0, 20.0)),
+ ]));
+ expect(logs, <String>['exitB', 'exitA']);
+ });
+
+ test('should trigger callbacks between disjoint siblings in correctly order', () {
+ // This test simulates the scenario of 2 sibling layers that do not overlap
+ // with each other.
+ //
+ // ———————— ————————
+ // |A | |B |
+ // | | | |
+ // ———————— ————————
+
+ bool isInA;
+ bool isInB;
+ final List<String> logs = <String>[];
+ final MouseTrackerAnnotation annotationA = MouseTrackerAnnotation(
+ onEnter: (PointerEnterEvent event) => logs.add('enterA'),
+ onExit: (PointerExitEvent event) => logs.add('exitA'),
+ onHover: (PointerHoverEvent event) => logs.add('hoverA'),
+ );
+ final MouseTrackerAnnotation annotationB = MouseTrackerAnnotation(
+ 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 annotationA;
+ } else if (isInB) {
+ yield annotationB;
+ }
+ });
+ _mouseTracker.attachAnnotation(annotationA);
+ _mouseTracker.attachAnnotation(annotationB);
+
+ // Starts within A
+ isInA = true;
+ isInB = false;
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.hover, const Offset(0.0, 1.0)),
+ ]));
+ expect(logs, <String>['enterA', 'hoverA']);
+ logs.clear();
+
+ // Moves into B within one frame
+ isInA = false;
+ isInB = true;
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.hover, const Offset(0.0, 10.0)),
+ ]));
+ expect(logs, <String>['exitA', 'enterB', 'hoverB']);
+ logs.clear();
+
+ // Moves into A within one frame
+ isInA = true;
+ isInB = false;
+ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+ _pointerData(PointerChange.hover, const Offset(0.0, 1.0)),
+ ]));
+ expect(logs, <String>['exitB', 'enterA', 'hoverA']);
});
}
diff --git a/packages/flutter/test/widgets/mouse_region_test.dart b/packages/flutter/test/widgets/mouse_region_test.dart
index c5f6683..d959175 100644
--- a/packages/flutter/test/widgets/mouse_region_test.dart
+++ b/packages/flutter/test/widgets/mouse_region_test.dart
@@ -836,7 +836,7 @@
// Move to the overlapping area
await gesture.moveTo(const Offset(75, 75));
await tester.pumpAndSettle();
- expect(logs, <String>['enterA', 'enterC', 'enterB']);
+ expect(logs, <String>['enterA', 'enterB', 'enterC']);
logs.clear();
// Move to the B only area
@@ -866,7 +866,7 @@
// Move out
await gesture.moveTo(const Offset(160, 160));
await tester.pumpAndSettle();
- expect(logs, <String>['exitA', 'exitB', 'exitC']);
+ expect(logs, <String>['exitC', 'exitB', 'exitA']);
});
testWidgets('an opaque one should prevent MouseRegions behind it receiving pointers', (WidgetTester tester) async {
@@ -890,13 +890,13 @@
// Move to the B only area
await gesture.moveTo(const Offset(25, 75));
await tester.pumpAndSettle();
- expect(logs, <String>['enterB', 'exitC']);
+ expect(logs, <String>['exitC', 'enterB']);
logs.clear();
// Move back to the overlapping area
await gesture.moveTo(const Offset(75, 75));
await tester.pumpAndSettle();
- expect(logs, <String>['enterC', 'exitB']);
+ expect(logs, <String>['exitB', 'enterC']);
logs.clear();
// Move to the C only area
@@ -914,7 +914,7 @@
// Move out
await gesture.moveTo(const Offset(160, 160));
await tester.pumpAndSettle();
- expect(logs, <String>['exitA', 'exitC']);
+ expect(logs, <String>['exitC', 'exitA']);
});
testWidgets('opaque should default to true', (WidgetTester tester) async {
@@ -938,7 +938,7 @@
// Move out
await gesture.moveTo(const Offset(160, 160));
await tester.pumpAndSettle();
- expect(logs, <String>['exitA', 'exitC']);
+ expect(logs, <String>['exitC', 'exitA']);
});
});