Unverified Commit 0aafdfa5 authored by Tong Mu's avatar Tong Mu Committed by GitHub

Redo: Rewrite MouseTracker's tracking and notifying algorithm (#42486)

* Revert "Revert "Rewrite MouseTracker's tracking and notifying algorithm (#42031)" (#42478)"

This reverts commit eede7929.

* Fix tests
parent 4dcf1ab8
...@@ -2435,7 +2435,7 @@ class ObjectFlagProperty<T> extends DiagnosticsProperty<T> { ...@@ -2435,7 +2435,7 @@ class ObjectFlagProperty<T> extends DiagnosticsProperty<T> {
/// ///
/// See also: /// 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. /// only one flag, and is preferred if there is only one entry.
/// * [IterableProperty], which provides similar functionality describing /// * [IterableProperty], which provides similar functionality describing
/// the values a collection of objects. /// the values a collection of objects.
......
...@@ -2,9 +2,10 @@ ...@@ -2,9 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:collection' show LinkedHashSet;
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/foundation.dart' show ChangeNotifier, visibleForTesting; import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'events.dart'; import 'events.dart';
...@@ -48,49 +49,82 @@ class MouseTrackerAnnotation { ...@@ -48,49 +49,82 @@ class MouseTrackerAnnotation {
@override @override
String toString() { String toString() {
final String none = (onEnter == null && onExit == null && onHover == null) ? ' <none>' : ''; final List<String> callbacks = <String>[];
return '[$runtimeType${hashCode.toRadixString(16)}$none' if (onEnter != null)
'${onEnter == null ? '' : ' onEnter'}' callbacks.add('enter');
'${onHover == null ? '' : ' onHover'}' if (onHover != null)
'${onExit == null ? '' : ' onExit'}]'; 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 /// Signature for searching for [MouseTrackerAnnotation]s at the given offset.
// 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.
/// ///
/// It is used by the [MouseTracker] to fetch annotations for the mouse /// It is used by the [MouseTracker] to fetch annotations for the mouse
/// position. /// position.
typedef MouseDetectorAnnotationFinder = Iterable<MouseTrackerAnnotation> Function(Offset offset); typedef MouseDetectorAnnotationFinder = Iterable<MouseTrackerAnnotation> Function(Offset offset);
/// Keeps state about which objects are interested in tracking mouse positions // Various states of each connected mouse device.
/// and notifies them when a mouse pointer enters, moves, or leaves an annotated //
/// region that they are interested in. // 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 /// This class is a [ChangeNotifier] that notifies its listeners if the value of
/// [mouseIsConnected] changes. /// [mouseIsConnected] changes.
/// ///
/// Owned by the [RendererBinding] class. /// An instance of [MouseTracker] is owned by the global singleton of
/// [RendererBinding].
class MouseTracker extends ChangeNotifier { class MouseTracker extends ChangeNotifier {
/// Creates a mouse tracker to keep track of mouse locations. /// 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. /// All of the parameters must not be null.
MouseTracker(this._router, this.annotationFinder) MouseTracker(this._router, this.annotationFinder)
: assert(_router != null), : assert(_router != null),
...@@ -104,102 +138,206 @@ class MouseTracker extends ChangeNotifier { ...@@ -104,102 +138,206 @@ class MouseTracker extends ChangeNotifier {
_router.removeGlobalRoute(_handleEvent); _router.removeGlobalRoute(_handleEvent);
} }
// The pointer router that the mouse tracker listens to for events. /// 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 pointer router that the mouse tracker listens to, and receives new
// mouse events from.
final PointerRouter _router; final PointerRouter _router;
/// Used to find annotations at a given logical coordinate. // Tracks the state of connected mouse devices.
final MouseDetectorAnnotationFinder annotationFinder; //
// It is the source of truth for the list of connected mouse devices.
final Map<int, _MouseState> _mouseStates = <int, _MouseState>{};
// The collection of annotations that are currently being tracked. They may or // Returns the mouse state of a device. If it doesn't exist, create one using
// may not be active, depending on the value of _TrackedAnnotation.active. // `mostRecentEvent`.
final Map<MouseTrackerAnnotation, _TrackedAnnotation> _trackedAnnotations = <MouseTrackerAnnotation, _TrackedAnnotation>{}; //
// 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;
}
/// Track an annotation so that if the mouse enters it, we send it events. // The collection of annotations that are currently being tracked.
/// // It is operated on by [attachAnnotation] and [detachAnnotation].
/// This is typically called when the [AnnotatedRegion] containing this final Set<MouseTrackerAnnotation> _trackedAnnotations = <MouseTrackerAnnotation>{};
/// annotation has been added to the layer tree. bool get _hasAttachedAnnotations => _trackedAnnotations.isNotEmpty;
void attachAnnotation(MouseTrackerAnnotation annotation) {
_trackedAnnotations[annotation] = _TrackedAnnotation(annotation); void _addMouseDevice(int device, PointerEvent event) {
// Schedule a check so that we test this new annotation to see if any mouse final bool wasConnected = mouseIsConnected;
// is currently inside its region. It has to happen after the frame is assert(!_mouseStates.containsKey(device));
// complete so that the annotation layer has been added before the check. _mouseStates[device] = _MouseState(mostRecentEvent: event);
if (mouseIsConnected) { // Schedule a check to enter annotations that might contain this pointer.
_scheduleMousePositionCheck(); _checkDeviceUpdates(device: device);
if (mouseIsConnected != wasConnected) {
notifyListeners();
} }
} }
/// Stops tracking an annotation, indicating that it has been removed from the void _removeMouseDevice(int device, PointerEvent event) {
/// layer tree. final bool wasConnected = mouseIsConnected;
/// assert(_mouseStates.containsKey(device));
/// An assertion error will be thrown if the associated layer is not removed final _MouseState disconnectedMouseState = _mouseStates.remove(device);
/// and receives another mouse hit. disconnectedMouseState.mostRecentEvent = event;
void detachAnnotation(MouseTrackerAnnotation annotation) { // Schedule a check to exit annotations that used to contain this pointer.
final _TrackedAnnotation trackedAnnotation = _findAnnotation(annotation); _checkDeviceUpdates(
for (int deviceId in trackedAnnotation.activeDevices) { device: device,
if (annotation.onExit != null) { disconnectedMouseState: disconnectedMouseState,
final PointerEvent event = _lastMouseEvent[deviceId] ?? _pendingRemovals[deviceId]; );
assert(event != null); if (mouseIsConnected != wasConnected) {
annotation.onExit(PointerExitEvent.fromMouseEvent(event)); notifyListeners();
}
}
// Handler for events coming from the PointerRouter.
void _handleEvent(PointerEvent event) {
if (event.kind != PointerDeviceKind.mouse) {
return;
}
final int device = event.device;
if (event is PointerAddedEvent) {
_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);
} }
} }
_trackedAnnotations.remove(annotation);
} }
bool _scheduledPostFramePositionCheck = false; bool _scheduledPostFramePositionCheck = false;
// Schedules a position check at the end of this frame for those annotations // Schedules a position check at the end of this frame.
// that have been added. // It is only called during a frame during which annotations have been added.
void _scheduleMousePositionCheck() { void _scheduleMousePositionCheck() {
// If we're not tracking anything, then there is no point in registering a // 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 // frame callback or scheduling a frame. By definition there are no active
// annotations that need exiting, either. // annotations that need exiting, either.
if (_trackedAnnotations.isNotEmpty && !_scheduledPostFramePositionCheck) { if (!_scheduledPostFramePositionCheck) {
_scheduledPostFramePositionCheck = true; _scheduledPostFramePositionCheck = true;
SchedulerBinding.instance.addPostFrameCallback((Duration duration) { SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
_sendMouseNotifications(_lastMouseEvent.keys); _checkAllDevicesUpdates();
_scheduledPostFramePositionCheck = false; _scheduledPostFramePositionCheck = false;
}); });
} }
} }
// Handler for events coming from the PointerRouter. // Collect the latest states of the given mouse device `device`, and call
void _handleEvent(PointerEvent event) { // interested callbacks.
if (event.kind != PointerDeviceKind.mouse) { //
return; // 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);
} }
final int deviceId = 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); // Dispatch callbacks related to a device after all necessary information
// If the mouse was removed, then we need to schedule one more check to // has been collected.
// exit any annotations that were active. //
_sendMouseNotifications(<int>{deviceId}); // This function should not change the provided states, and should not access
} else { // information that is not provided in parameters (hence being static).
if (event is PointerMoveEvent || event is PointerHoverEvent || event is PointerDownEvent) { static void _dispatchDeviceCallbacks({
final PointerEvent lastEvent = _lastMouseEvent[deviceId]; @required LinkedHashSet<MouseTrackerAnnotation> nextAnnotations,
_addMouseEvent(deviceId, event); @required _MouseState currentState,
if (lastEvent == null || }) {
lastEvent is PointerAddedEvent || lastEvent.position != event.position) { // Order is important for mouse event callbacks. The `findAnnotations`
// Only schedule a frame if we have our first event, or if the // returns annotations in the visual order from front to back. We call
// location of the mouse has changed, and only if there are tracked annotations. // it the "visual order", and the opposite one "reverse visual order".
_sendMouseNotifications(<int>{deviceId}); // 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));
} }
} }
_TrackedAnnotation _findAnnotation(MouseTrackerAnnotation annotation) { // Send hover events in reverse visual order.
final _TrackedAnnotation trackedAnnotation = _trackedAnnotations[annotation]; // For now the order between the hover events is designed this way for no
assert( // solid reasons but to keep it aligned with enter events for simplicity.
trackedAnnotation != null, if (mostRecentEvent is PointerHoverEvent) {
'Unable to find annotation $annotation in tracked annotations. ' final Iterable<MouseTrackerAnnotation> hoveringAnnotations =
'Check that attachAnnotation has been called for all annotated layers.'); nextAnnotations.toList().reversed;
return trackedAnnotation; for (final MouseTrackerAnnotation annotation in hoveringAnnotations) {
if (annotation.onHover != null) {
annotation.onHover(mostRecentEvent);
}
}
}
} }
/// Checks if the given [MouseTrackerAnnotation] is attached to this /// Checks if the given [MouseTrackerAnnotation] is attached to this
...@@ -209,127 +347,59 @@ class MouseTracker extends ChangeNotifier { ...@@ -209,127 +347,59 @@ class MouseTracker extends ChangeNotifier {
/// MouseTracker. Do not call in other contexts. /// MouseTracker. Do not call in other contexts.
@visibleForTesting @visibleForTesting
bool isAnnotationAttached(MouseTrackerAnnotation annotation) { 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 /// Whether or not a mouse is connected and has produced events.
// exit to all active tracked annotations, since none of them were hit. bool get mouseIsConnected => _mouseStates.isNotEmpty;
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(); /// Notify [MouseTracker] that a new mouse tracker annotation has started to
for (_TrackedAnnotation hitAnnotation in hitAnnotations) { /// take effect.
if (!hitAnnotation.activeDevices.contains(deviceId)) { ///
// A tracked annotation that just became active and needs to have an enter /// This should be called as soon as the layer that owns this annotation is
// event sent to it. /// added to the layer tree.
hitAnnotation.activeDevices.add(deviceId); ///
if (hitAnnotation.annotation?.onEnter != null) { /// This triggers [MouseTracker] to schedule a mouse position check during the
hitAnnotation.annotation.onEnter(PointerEnterEvent.fromMouseEvent(lastEvent)); /// 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();
} }
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) { /// Notify [MouseTracker] that a mouse tracker annotation that was previously
final bool wasConnected = mouseIsConnected; /// attached has stopped taking effect.
if (event is PointerAddedEvent) { ///
// If we are adding the device again, then we're not removing it anymore. /// This should be called as soon as the layer that owns this annotation is
_pendingRemovals.remove(deviceId); /// removed from the layer tree. An assertion error will be thrown if the
} /// associated layer is not removed and receives another mouse hit.
_lastMouseEvent[deviceId] = event; ///
if (mouseIsConnected != wasConnected) { /// This triggers [MouseTracker] to perform a mouse position check immediately
notifyListeners(); /// 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);
void _removeMouseEvent(int deviceId, PointerEvent event) {
final bool wasConnected = mouseIsConnected;
assert(event is PointerRemovedEvent);
_pendingRemovals[deviceId] = event;
_lastMouseEvent.remove(deviceId);
if (mouseIsConnected != wasConnected) {
notifyListeners();
} }
});
_trackedAnnotations.remove(annotation);
} }
// 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;
} }
...@@ -16,211 +16,579 @@ import '../flutter_test_alternative.dart'; ...@@ -16,211 +16,579 @@ import '../flutter_test_alternative.dart';
typedef HandleEventCallback = void Function(PointerEvent event); typedef HandleEventCallback = void Function(PointerEvent event);
class TestGestureFlutterBinding extends BindingBase with ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, RendererBinding { class _TestGestureFlutterBinding extends BindingBase
HandleEventCallback callback; with ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, RendererBinding {
@override
void initInstances() {
super.initInstances();
postFrameCallbacks = <void Function(Duration)>[];
}
List<void Function(Duration)> postFrameCallbacks;
// Proxy post-frame callbacks
@override @override
void handleEvent(PointerEvent event, HitTestEntry entry) { void addPostFrameCallback(void Function(Duration) callback) {
super.handleEvent(event, entry); postFrameCallbacks.add(callback);
if (callback != null) { }
callback(event);
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() { void _ensureTestGestureBinding() {
_binding ??= TestGestureFlutterBinding(); _binding ??= _TestGestureFlutterBinding();
assert(GestureBinding.instance != null); assert(GestureBinding.instance != null);
} }
void main() { void main() {
setUp(ensureTestGestureBinding); void _setUpMouseAnnotationFinder(MouseDetectorAnnotationFinder annotationFinder) {
final MouseTracker mouseTracker = MouseTracker(
final List<PointerEvent> events = <PointerEvent>[]; GestureBinding.instance.pointerRouter,
final MouseTrackerAnnotation annotation = MouseTrackerAnnotation( annotationFinder,
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; RendererBinding.instance.initMouseTracker(mouseTracker);
bool isInHitRegionTwo;
void clear() {
events.clear();
} }
setUp(() { // Set up a trivial test environment that includes one annotation, which adds
clear(); // the enter, hover, and exit events it received to [logEvents].
isInHitRegionOne = true; MouseTrackerAnnotation _setUpWithOneAnnotation({List<PointerEvent> logEvents}) {
isInHitRegionTwo = false; final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
RendererBinding.instance.initMouseTracker( onEnter: (PointerEnterEvent event) => logEvents.add(event),
MouseTracker( onHover: (PointerHoverEvent event) => logEvents.add(event),
GestureBinding.instance.pointerRouter, onExit: (PointerExitEvent event) => logEvents.add(event),
);
_setUpMouseAnnotationFinder(
(Offset position) sync* { (Offset position) sync* {
if (isInHitRegionOne)
yield annotation; yield annotation;
else if (isInHitRegionTwo) {
yield partialAnnotation;
}
}, },
),
); );
_mouseTracker.attachAnnotation(annotation);
return annotation;
}
setUp(() {
_ensureTestGestureBinding();
_binding.postFrameCallbacks.clear();
PointerEventConverter.clearPointers(); PointerEventConverter.clearPointers();
}); });
test('receives and processes mouse hover events', () { test('MouseTrackerAnnotation has correct toString', () {
final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[ final MouseTrackerAnnotation annotation1 = MouseTrackerAnnotation(
// Will implicitly also add a PointerAdded event. onEnter: (_) {},
_pointerData(PointerChange.hover, const Offset(0.0, 0.0)), onExit: (_) {},
]); onHover: (_) {},
final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[ );
_pointerData(PointerChange.hover, const Offset(1.0, 101.0)), expect(
]); annotation1.toString(),
final ui.PointerDataPacket packet3 = ui.PointerDataPacket(data: <ui.PointerData>[ equals('MouseTrackerAnnotation#${shortHash(annotation1)}(callbacks: enter hover exit)'),
_pointerData(PointerChange.remove, const Offset(1.0, 201.0)), );
]);
final ui.PointerDataPacket packet4 = ui.PointerDataPacket(data: <ui.PointerData>[ const MouseTrackerAnnotation annotation2 = MouseTrackerAnnotation();
_pointerData(PointerChange.hover, const Offset(1.0, 301.0)), expect(
]); annotation2.toString(),
final ui.PointerDataPacket packet5 = ui.PointerDataPacket(data: <ui.PointerData>[ equals('MouseTrackerAnnotation#${shortHash(annotation2)}(callbacks: <none>)'),
_pointerData(PointerChange.hover, const Offset(1.0, 401.0), device: 1), );
]); });
RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
isInHitRegionOne = true; test('should detect enter, hover, and exit from Added, Hover, and Removed events', () {
ui.window.onPointerDataPacket(packet1); 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>[ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerEnterEvent(position: Offset(0.0, 0.0)), const PointerEnterEvent(position: Offset(1.0, 0.0)),
const PointerHoverEvent(position: Offset(0.0, 0.0)), const PointerHoverEvent(position: Offset(1.0, 0.0)),
])); ]));
clear(); expect(listenerLogs, <bool>[true]);
events.clear();
listenerLogs.clear();
ui.window.onPointerDataPacket(packet2); // Hover
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
]));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerHoverEvent(position: Offset(1.0, 101.0)), 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>[ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerHoverEvent(position: Offset(1.0, 201.0)), const PointerHoverEvent(position: Offset(1.0, 201.0)),
const PointerExitEvent(position: Offset(1.0, 201.0)), const PointerExitEvent(position: Offset(1.0, 201.0)),
])); ]));
expect(listenerLogs, <bool>[false]);
events.clear();
listenerLogs.clear();
clear(); // Add again
ui.window.onPointerDataPacket(packet4); ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(1.0, 301.0)),
]));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerEnterEvent(position: Offset(1.0, 301.0)), const PointerEnterEvent(position: Offset(1.0, 301.0)),
const PointerHoverEvent(position: Offset(1.0, 301.0)), const PointerHoverEvent(position: Offset(1.0, 301.0)),
])); ]));
expect(listenerLogs, <bool>[true]);
events.clear();
listenerLogs.clear();
});
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();
// add in a second mouse simultaneously. // Second mouse
clear(); ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
ui.window.onPointerDataPacket(packet5); _pointerData(PointerChange.hover, const Offset(1.0, 401.0), device: 1),
]));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerEnterEvent(position: Offset(1.0, 401.0), device: 1), const PointerEnterEvent(position: Offset(1.0, 401.0), device: 1),
const PointerHoverEvent(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', () { test('should handle detaching during the callback of exiting', () {
final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[ bool isInHitRegion;
_pointerData(PointerChange.hover, const Offset(0.0, 0.0)), final List<PointerEvent> events = <PointerEvent>[];
_pointerData(PointerChange.hover, const Offset(1.0, 101.0)), final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
]); onEnter: (PointerEnterEvent event) => events.add(event),
final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[ onHover: (PointerHoverEvent event) => events.add(event),
_pointerData(PointerChange.hover, const Offset(1.0, 201.0)), onExit: (PointerExitEvent event) => events.add(event),
]); );
isInHitRegionOne = true; _setUpMouseAnnotationFinder((Offset position) sync* {
RendererBinding.instance.mouseTracker.attachAnnotation(annotation); if (isInHitRegion) {
yield annotation;
}
});
ui.window.onPointerDataPacket(packet1); isInHitRegion = true;
_mouseTracker.attachAnnotation(annotation);
// Enter
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(1.0, 0.0)),
]));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerEnterEvent(position: Offset(0.0, 0.0)), const PointerEnterEvent(position: Offset(1.0, 0.0)),
const PointerHoverEvent(position: Offset(0.0, 0.0)), const PointerHoverEvent(position: Offset(1.0, 0.0)),
const PointerHoverEvent(position: Offset(1.0, 101.0)),
])); ]));
// Simulate layer going away by detaching it. expect(_mouseTracker.mouseIsConnected, isTrue);
clear(); events.clear();
isInHitRegionOne = false;
ui.window.onPointerDataPacket(packet2); // 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>[ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerExitEvent(position: Offset(1.0, 201.0)), const PointerExitEvent(position: Offset(1.0, 0.0)),
]));
expect(_mouseTracker.mouseIsConnected, isFalse);
events.clear();
});
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();
// Actually detach annotation. Shouldn't receive hit. ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
RendererBinding.instance.mouseTracker.detachAnnotation(annotation); _pointerData(PointerChange.move, const Offset(0.0, 201.0)),
clear(); ]));
isInHitRegionOne = false; 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();
ui.window.onPointerDataPacket(packet2); // 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>[ 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>[ final ui.PointerDataPacket packet = ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(1.0, 101.0)), _pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
]); ]);
isInHitRegionOne = false; isInHitRegionOne = false;
isInHitRegionTwo = true; isInHitRegionTwo = true;
RendererBinding.instance.mouseTracker.attachAnnotation(partialAnnotation); _mouseTracker.attachAnnotation(annotation2);
ui.window.onPointerDataPacket(packet); ui.window.onPointerDataPacket(packet);
RendererBinding.instance.mouseTracker.detachAnnotation(partialAnnotation); _mouseTracker.detachAnnotation(annotation2);
isInHitRegionTwo = false; isInHitRegionTwo = false;
// Passes if no errors are thrown // Passes if no errors are thrown
}); });
test('detects exit when mouse goes away', () { test('should not call annotationFinder when no annotations are attached', () {
final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[ final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
_pointerData(PointerChange.hover, const Offset(0.0, 0.0)), onEnter: (PointerEnterEvent event) {},
_pointerData(PointerChange.hover, const Offset(1.0, 101.0)), );
]); int finderCalled = 0;
final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[ _setUpMouseAnnotationFinder((Offset position) sync* {
_pointerData(PointerChange.remove, const Offset(1.0, 201.0)), finderCalled++;
]); // This annotation is never in the region
isInHitRegionOne = true; });
RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
ui.window.onPointerDataPacket(packet1); // When no annotations are attached, hovering should not call finder.
ui.window.onPointerDataPacket(packet2); ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[ _pointerData(PointerChange.hover, const Offset(0.0, 101.0)),
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)),
])); ]));
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', () { test('should trigger callbacks between parents and children in correct order', () {
final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[ // This test simulates the scenario of a layer being the child of another.
_pointerData(PointerChange.hover, const Offset(0.0, 0.0)), //
_pointerData(PointerChange.hover, const Offset(1.0, 101.0)), // ———————————
]); // |A |
final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[ // | —————— |
_pointerData(PointerChange.down, const Offset(1.0, 101.0)), // | |B | |
_pointerData(PointerChange.move, const Offset(1.0, 201.0)), // | —————— |
]); // ———————————
isInHitRegionOne = true;
RendererBinding.instance.mouseTracker.attachAnnotation(annotation); bool isInB;
ui.window.onPointerDataPacket(packet1); final List<String> logs = <String>[];
ui.window.onPointerDataPacket(packet2); final MouseTrackerAnnotation annotationA = MouseTrackerAnnotation(
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[ onEnter: (PointerEnterEvent event) => logs.add('enterA'),
const PointerEnterEvent(position: Offset(0.0, 0.0), delta: Offset(0.0, 0.0)), onExit: (PointerExitEvent event) => logs.add('exitA'),
const PointerHoverEvent(position: Offset(0.0, 0.0), delta: Offset(0.0, 0.0)), onHover: (PointerHoverEvent event) => logs.add('hoverA'),
const PointerHoverEvent(position: Offset(1.0, 101.0), delta: Offset(1.0, 101.0)), );
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']);
}); });
} }
......
...@@ -836,7 +836,7 @@ void main() { ...@@ -836,7 +836,7 @@ void main() {
// Move to the overlapping area // Move to the overlapping area
await gesture.moveTo(const Offset(75, 75)); await gesture.moveTo(const Offset(75, 75));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(logs, <String>['enterA', 'enterC', 'enterB']); expect(logs, <String>['enterA', 'enterB', 'enterC']);
logs.clear(); logs.clear();
// Move to the B only area // Move to the B only area
...@@ -866,7 +866,7 @@ void main() { ...@@ -866,7 +866,7 @@ void main() {
// Move out // Move out
await gesture.moveTo(const Offset(160, 160)); await gesture.moveTo(const Offset(160, 160));
await tester.pumpAndSettle(); 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 { testWidgets('an opaque one should prevent MouseRegions behind it receiving pointers', (WidgetTester tester) async {
...@@ -890,13 +890,13 @@ void main() { ...@@ -890,13 +890,13 @@ void main() {
// Move to the B only area // Move to the B only area
await gesture.moveTo(const Offset(25, 75)); await gesture.moveTo(const Offset(25, 75));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(logs, <String>['enterB', 'exitC']); expect(logs, <String>['exitC', 'enterB']);
logs.clear(); logs.clear();
// Move back to the overlapping area // Move back to the overlapping area
await gesture.moveTo(const Offset(75, 75)); await gesture.moveTo(const Offset(75, 75));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(logs, <String>['enterC', 'exitB']); expect(logs, <String>['exitB', 'enterC']);
logs.clear(); logs.clear();
// Move to the C only area // Move to the C only area
...@@ -914,7 +914,7 @@ void main() { ...@@ -914,7 +914,7 @@ void main() {
// Move out // Move out
await gesture.moveTo(const Offset(160, 160)); await gesture.moveTo(const Offset(160, 160));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(logs, <String>['exitA', 'exitC']); expect(logs, <String>['exitC', 'exitA']);
}); });
testWidgets('opaque should default to true', (WidgetTester tester) async { testWidgets('opaque should default to true', (WidgetTester tester) async {
...@@ -938,7 +938,7 @@ void main() { ...@@ -938,7 +938,7 @@ void main() {
// Move out // Move out
await gesture.moveTo(const Offset(160, 160)); await gesture.moveTo(const Offset(160, 160));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(logs, <String>['exitA', 'exitC']); expect(logs, <String>['exitC', 'exitA']);
}); });
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment