mirror of
https://github.com/flutter/flutter
synced 2024-10-13 11:42:54 +00:00
This commit is contained in:
parent
2b635816c6
commit
eede792923
|
@ -2435,7 +2435,7 @@ class ObjectFlagProperty<T> extends DiagnosticsProperty<T> {
|
||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [ObjectFlagProperty], which provides similar functionality but accepts
|
/// * [ObjectFlagSummary], 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,10 +2,9 @@
|
||||||
// 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';
|
import 'package:flutter/foundation.dart' show ChangeNotifier, visibleForTesting;
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
|
|
||||||
import 'events.dart';
|
import 'events.dart';
|
||||||
|
@ -49,82 +48,49 @@ class MouseTrackerAnnotation {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
final List<String> callbacks = <String>[];
|
final String none = (onEnter == null && onExit == null && onHover == null) ? ' <none>' : '';
|
||||||
if (onEnter != null)
|
return '[$runtimeType${hashCode.toRadixString(16)}$none'
|
||||||
callbacks.add('enter');
|
'${onEnter == null ? '' : ' onEnter'}'
|
||||||
if (onHover != null)
|
'${onHover == null ? '' : ' onHover'}'
|
||||||
callbacks.add('hover');
|
'${onExit == null ? '' : ' onExit'}]';
|
||||||
if (onExit != null)
|
|
||||||
callbacks.add('exit');
|
|
||||||
final String describeCallbacks = callbacks.isEmpty
|
|
||||||
? '<none>'
|
|
||||||
: callbacks.join(' ');
|
|
||||||
return '${describeIdentity(this)}(callbacks: $describeCallbacks)';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Signature for searching for [MouseTrackerAnnotation]s at the given offset.
|
// 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.
|
||||||
///
|
///
|
||||||
/// 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);
|
||||||
|
|
||||||
// Various states of each connected mouse device.
|
/// Keeps state about which objects are interested in tracking mouse positions
|
||||||
//
|
/// and notifies them when a mouse pointer enters, moves, or leaves an annotated
|
||||||
// It is used by [MouseTracker] to compute which callbacks should be triggered
|
/// region that they are interested in.
|
||||||
// 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.
|
||||||
///
|
///
|
||||||
/// An instance of [MouseTracker] is owned by the global singleton of
|
/// Owned by the [RendererBinding] class.
|
||||||
/// [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),
|
||||||
|
@ -138,67 +104,60 @@ class MouseTracker extends ChangeNotifier {
|
||||||
_router.removeGlobalRoute(_handleEvent);
|
_router.removeGlobalRoute(_handleEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find annotations at a given offset in global logical coordinate space
|
// The pointer router that the mouse tracker listens to for events.
|
||||||
/// 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;
|
||||||
|
|
||||||
// Tracks the state of connected mouse devices.
|
/// Used to find annotations at a given logical coordinate.
|
||||||
//
|
final MouseDetectorAnnotationFinder annotationFinder;
|
||||||
// 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
|
// The collection of annotations that are currently being tracked. They may or
|
||||||
// `mostRecentEvent`.
|
// may not be active, depending on the value of _TrackedAnnotation.active.
|
||||||
//
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The collection of annotations that are currently being tracked.
|
/// Track an annotation so that if the mouse enters it, we send it events.
|
||||||
// It is operated on by [attachAnnotation] and [detachAnnotation].
|
///
|
||||||
final Set<MouseTrackerAnnotation> _trackedAnnotations = <MouseTrackerAnnotation>{};
|
/// This is typically called when the [AnnotatedRegion] containing this
|
||||||
bool get _hasAttachedAnnotations => _trackedAnnotations.isNotEmpty;
|
/// annotation has been added to the layer tree.
|
||||||
|
void attachAnnotation(MouseTrackerAnnotation annotation) {
|
||||||
void _addMouseDevice(int device, PointerEvent event) {
|
_trackedAnnotations[annotation] = _TrackedAnnotation(annotation);
|
||||||
final bool wasConnected = mouseIsConnected;
|
// Schedule a check so that we test this new annotation to see if any mouse
|
||||||
assert(!_mouseStates.containsKey(device));
|
// is currently inside its region. It has to happen after the frame is
|
||||||
_mouseStates[device] = _MouseState(mostRecentEvent: event);
|
// complete so that the annotation layer has been added before the check.
|
||||||
// Schedule a check to enter annotations that might contain this pointer.
|
if (mouseIsConnected) {
|
||||||
_checkDeviceUpdates(device: device);
|
_scheduleMousePositionCheck();
|
||||||
if (mouseIsConnected != wasConnected) {
|
|
||||||
notifyListeners();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _removeMouseDevice(int device, PointerEvent event) {
|
/// Stops tracking an annotation, indicating that it has been removed from the
|
||||||
final bool wasConnected = mouseIsConnected;
|
/// layer tree.
|
||||||
assert(_mouseStates.containsKey(device));
|
///
|
||||||
final _MouseState disconnectedMouseState = _mouseStates.remove(device);
|
/// An assertion error will be thrown if the associated layer is not removed
|
||||||
disconnectedMouseState.mostRecentEvent = event;
|
/// and receives another mouse hit.
|
||||||
// Schedule a check to exit annotations that used to contain this pointer.
|
void detachAnnotation(MouseTrackerAnnotation annotation) {
|
||||||
_checkDeviceUpdates(
|
final _TrackedAnnotation trackedAnnotation = _findAnnotation(annotation);
|
||||||
device: device,
|
for (int deviceId in trackedAnnotation.activeDevices) {
|
||||||
disconnectedMouseState: disconnectedMouseState,
|
if (annotation.onExit != null) {
|
||||||
);
|
final PointerEvent event = _lastMouseEvent[deviceId] ?? _pendingRemovals[deviceId];
|
||||||
if (mouseIsConnected != wasConnected) {
|
assert(event != null);
|
||||||
notifyListeners();
|
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;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,139 +166,42 @@ class MouseTracker extends ChangeNotifier {
|
||||||
if (event.kind != PointerDeviceKind.mouse) {
|
if (event.kind != PointerDeviceKind.mouse) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final int device = event.device;
|
final int deviceId = event.device;
|
||||||
if (event is PointerAddedEvent) {
|
if (event is PointerAddedEvent) {
|
||||||
_addMouseDevice(device, event);
|
// If we are adding the device again, then we're not removing it anymore.
|
||||||
} else if (event is PointerRemovedEvent) {
|
_pendingRemovals.remove(deviceId);
|
||||||
_removeMouseDevice(device, event);
|
_addMouseEvent(deviceId, event);
|
||||||
} else if (event is PointerHoverEvent) {
|
_sendMouseNotifications(<int>{deviceId});
|
||||||
final _MouseState mouseState = _guaranteeMouseState(device, event);
|
return;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
if (event is PointerRemovedEvent) {
|
||||||
|
_removeMouseEvent(deviceId, event);
|
||||||
bool _scheduledPostFramePositionCheck = false;
|
// If the mouse was removed, then we need to schedule one more check to
|
||||||
// Schedules a position check at the end of this frame.
|
// exit any annotations that were active.
|
||||||
// It is only called during a frame during which annotations have been added.
|
_sendMouseNotifications(<int>{deviceId});
|
||||||
void _scheduleMousePositionCheck() {
|
} else {
|
||||||
// If we're not tracking anything, then there is no point in registering a
|
if (event is PointerMoveEvent || event is PointerHoverEvent || event is PointerDownEvent) {
|
||||||
// frame callback or scheduling a frame. By definition there are no active
|
final PointerEvent lastEvent = _lastMouseEvent[deviceId];
|
||||||
// annotations that need exiting, either.
|
_addMouseEvent(deviceId, event);
|
||||||
if (!_scheduledPostFramePositionCheck) {
|
if (lastEvent == null ||
|
||||||
_scheduledPostFramePositionCheck = true;
|
lastEvent is PointerAddedEvent || lastEvent.position != event.position) {
|
||||||
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
|
// Only schedule a frame if we have our first event, or if the
|
||||||
_checkAllDevicesUpdates();
|
// location of the mouse has changed, and only if there are tracked annotations.
|
||||||
_scheduledPostFramePositionCheck = false;
|
_sendMouseNotifications(<int>{deviceId});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_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;
|
||||||
|
}
|
||||||
|
|
||||||
/// Checks if the given [MouseTrackerAnnotation] is attached to this
|
/// Checks if the given [MouseTrackerAnnotation] is attached to this
|
||||||
/// [MouseTracker].
|
/// [MouseTracker].
|
||||||
///
|
///
|
||||||
|
@ -347,59 +209,127 @@ 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.contains(annotation);
|
return _trackedAnnotations.containsKey(annotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether or not a mouse is connected and has produced events.
|
// Tells interested objects that a mouse has entered, exited, or moved, given
|
||||||
bool get mouseIsConnected => _mouseStates.isNotEmpty;
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
/// Notify [MouseTracker] that a new mouse tracker annotation has started to
|
void exitAnnotation(_TrackedAnnotation trackedAnnotation, int deviceId) {
|
||||||
/// take effect.
|
if (trackedAnnotation.annotation?.onExit != null && trackedAnnotation.activeDevices.contains(deviceId)) {
|
||||||
///
|
final PointerEvent event = _lastMouseEvent[deviceId] ?? _pendingRemovals[deviceId];
|
||||||
/// This should be called as soon as the layer that owns this annotation is
|
assert(event != null);
|
||||||
/// added to the layer tree.
|
trackedAnnotation.annotation.onExit(PointerExitEvent.fromMouseEvent(event));
|
||||||
///
|
}
|
||||||
/// This triggers [MouseTracker] to schedule a mouse position check during the
|
trackedAnnotation.activeDevices.remove(deviceId);
|
||||||
/// post frame to see if this new annotation might trigger enter events.
|
}
|
||||||
///
|
|
||||||
/// The [MouseTracker] also uses this to track the number of attached
|
void exitAllDevices(_TrackedAnnotation trackedAnnotation) {
|
||||||
/// annotations, and will skip mouse position checks if there is no
|
if (trackedAnnotation.activeDevices.isNotEmpty) {
|
||||||
/// annotations attached.
|
final Set<int> deviceIds = trackedAnnotation.activeDevices.toSet();
|
||||||
void attachAnnotation(MouseTrackerAnnotation annotation) {
|
for (int deviceId in deviceIds) {
|
||||||
// Schedule a check so that we test this new annotation to see if any mouse
|
exitAnnotation(trackedAnnotation, deviceId);
|
||||||
// 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();
|
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) {
|
||||||
/// 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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
_trackedAnnotations.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,579 +16,211 @@ import '../flutter_test_alternative.dart';
|
||||||
|
|
||||||
typedef HandleEventCallback = void Function(PointerEvent event);
|
typedef HandleEventCallback = void Function(PointerEvent event);
|
||||||
|
|
||||||
class _TestGestureFlutterBinding extends BindingBase
|
class TestGestureFlutterBinding extends BindingBase with ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, RendererBinding {
|
||||||
with ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, RendererBinding {
|
HandleEventCallback callback;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initInstances() {
|
void handleEvent(PointerEvent event, HitTestEntry entry) {
|
||||||
super.initInstances();
|
super.handleEvent(event, entry);
|
||||||
postFrameCallbacks = <void Function(Duration)>[];
|
if (callback != null) {
|
||||||
}
|
callback(event);
|
||||||
|
|
||||||
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() {
|
void ensureTestGestureBinding() {
|
||||||
_binding ??= _TestGestureFlutterBinding();
|
_binding ??= TestGestureFlutterBinding();
|
||||||
assert(GestureBinding.instance != null);
|
assert(GestureBinding.instance != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
void _setUpMouseAnnotationFinder(MouseDetectorAnnotationFinder annotationFinder) {
|
setUp(ensureTestGestureBinding);
|
||||||
final MouseTracker mouseTracker = MouseTracker(
|
|
||||||
GestureBinding.instance.pointerRouter,
|
|
||||||
annotationFinder,
|
|
||||||
);
|
|
||||||
RendererBinding.instance.initMouseTracker(mouseTracker);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up a trivial test environment that includes one annotation, which adds
|
final List<PointerEvent> events = <PointerEvent>[];
|
||||||
// the enter, hover, and exit events it received to [logEvents].
|
final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
|
||||||
MouseTrackerAnnotation _setUpWithOneAnnotation({List<PointerEvent> logEvents}) {
|
onEnter: (PointerEnterEvent event) => events.add(event),
|
||||||
final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
|
onHover: (PointerHoverEvent event) => events.add(event),
|
||||||
onEnter: (PointerEnterEvent event) => logEvents.add(event),
|
onExit: (PointerExitEvent event) => events.add(event),
|
||||||
onHover: (PointerHoverEvent event) => logEvents.add(event),
|
);
|
||||||
onExit: (PointerExitEvent event) => logEvents.add(event),
|
// Only respond to some mouse events.
|
||||||
);
|
final MouseTrackerAnnotation partialAnnotation = MouseTrackerAnnotation(
|
||||||
_setUpMouseAnnotationFinder(
|
onEnter: (PointerEnterEvent event) => events.add(event),
|
||||||
(Offset position) sync* {
|
onHover: (PointerHoverEvent event) => events.add(event),
|
||||||
yield annotation;
|
);
|
||||||
},
|
bool isInHitRegionOne;
|
||||||
);
|
bool isInHitRegionTwo;
|
||||||
_mouseTracker.attachAnnotation(annotation);
|
|
||||||
return annotation;
|
void clear() {
|
||||||
|
events.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
_ensureTestGestureBinding();
|
clear();
|
||||||
_binding.postFrameCallbacks.clear();
|
isInHitRegionOne = true;
|
||||||
|
isInHitRegionTwo = false;
|
||||||
|
RendererBinding.instance.initMouseTracker(
|
||||||
|
MouseTracker(
|
||||||
|
GestureBinding.instance.pointerRouter,
|
||||||
|
(Offset position) sync* {
|
||||||
|
if (isInHitRegionOne)
|
||||||
|
yield annotation;
|
||||||
|
else if (isInHitRegionTwo) {
|
||||||
|
yield partialAnnotation;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
PointerEventConverter.clearPointers();
|
PointerEventConverter.clearPointers();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('MouseTrackerAnnotation has correct toString', () {
|
test('receives and processes mouse hover events', () {
|
||||||
final MouseTrackerAnnotation annotation1 = MouseTrackerAnnotation(
|
final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[
|
||||||
onEnter: (_) {},
|
// Will implicitly also add a PointerAdded event.
|
||||||
onExit: (_) {},
|
_pointerData(PointerChange.hover, const Offset(0.0, 0.0)),
|
||||||
onHover: (_) {},
|
]);
|
||||||
);
|
final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[
|
||||||
expect(
|
|
||||||
annotation1.toString(),
|
|
||||||
equals('MouseTrackerAnnotation#${shortHash(annotation1)}(callbacks: enter hover exit)'),
|
|
||||||
);
|
|
||||||
|
|
||||||
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)),
|
_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();
|
||||||
|
|
||||||
|
ui.window.onPointerDataPacket(packet2);
|
||||||
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
|
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
|
||||||
const PointerHoverEvent(position: Offset(1.0, 101.0)),
|
const PointerHoverEvent(position: Offset(1.0, 101.0)),
|
||||||
]));
|
]));
|
||||||
expect(_mouseTracker.mouseIsConnected, isTrue);
|
clear();
|
||||||
expect(listenerLogs, <bool>[]);
|
|
||||||
events.clear();
|
|
||||||
|
|
||||||
// Remove
|
ui.window.onPointerDataPacket(packet3);
|
||||||
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();
|
|
||||||
|
|
||||||
// Add again
|
clear();
|
||||||
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
|
ui.window.onPointerDataPacket(packet4);
|
||||||
_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', () {
|
// add in a second mouse simultaneously.
|
||||||
final List<PointerEvent> events = <PointerEvent>[];
|
clear();
|
||||||
_setUpWithOneAnnotation(logEvents: events);
|
ui.window.onPointerDataPacket(packet5);
|
||||||
|
|
||||||
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>[
|
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('should handle detaching during the callback of exiting', () {
|
test('detects exit when annotated layer no longer hit', () {
|
||||||
bool isInHitRegion;
|
final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[
|
||||||
final List<PointerEvent> events = <PointerEvent>[];
|
_pointerData(PointerChange.hover, const Offset(0.0, 0.0)),
|
||||||
final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
|
_pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
|
||||||
onEnter: (PointerEnterEvent event) => events.add(event),
|
]);
|
||||||
onHover: (PointerHoverEvent event) => events.add(event),
|
final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[
|
||||||
onExit: (PointerExitEvent event) => events.add(event),
|
_pointerData(PointerChange.hover, const Offset(1.0, 201.0)),
|
||||||
);
|
]);
|
||||||
_setUpMouseAnnotationFinder((Offset position) sync* {
|
isInHitRegionOne = true;
|
||||||
if (isInHitRegion) {
|
RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
|
||||||
yield annotation;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
isInHitRegion = true;
|
ui.window.onPointerDataPacket(packet1);
|
||||||
_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(1.0, 0.0)),
|
const PointerEnterEvent(position: Offset(0.0, 0.0)),
|
||||||
const PointerHoverEvent(position: Offset(1.0, 0.0)),
|
const PointerHoverEvent(position: Offset(0.0, 0.0)),
|
||||||
|
const PointerHoverEvent(position: Offset(1.0, 101.0)),
|
||||||
]));
|
]));
|
||||||
expect(_mouseTracker.mouseIsConnected, isTrue);
|
// Simulate layer going away by detaching it.
|
||||||
events.clear();
|
clear();
|
||||||
|
isInHitRegionOne = false;
|
||||||
|
|
||||||
// Remove
|
ui.window.onPointerDataPacket(packet2);
|
||||||
_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, 0.0)),
|
const PointerExitEvent(position: Offset(1.0, 201.0)),
|
||||||
]));
|
]));
|
||||||
expect(_mouseTracker.mouseIsConnected, isFalse);
|
|
||||||
events.clear();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not handle non-hover events', () {
|
// Actually detach annotation. Shouldn't receive hit.
|
||||||
final List<PointerEvent> events = <PointerEvent>[];
|
RendererBinding.instance.mouseTracker.detachAnnotation(annotation);
|
||||||
_setUpWithOneAnnotation(logEvents: events);
|
clear();
|
||||||
|
isInHitRegionOne = false;
|
||||||
|
|
||||||
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
|
ui.window.onPointerDataPacket(packet2);
|
||||||
_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>[
|
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
|
||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not flip out if not all mouse events are listened to', () {
|
test("don't 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;
|
||||||
_mouseTracker.attachAnnotation(annotation2);
|
RendererBinding.instance.mouseTracker.attachAnnotation(partialAnnotation);
|
||||||
|
|
||||||
ui.window.onPointerDataPacket(packet);
|
ui.window.onPointerDataPacket(packet);
|
||||||
_mouseTracker.detachAnnotation(annotation2);
|
RendererBinding.instance.mouseTracker.detachAnnotation(partialAnnotation);
|
||||||
isInHitRegionTwo = false;
|
isInHitRegionTwo = false;
|
||||||
|
|
||||||
// Passes if no errors are thrown
|
// Passes if no errors are thrown
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not call annotationFinder when no annotations are attached', () {
|
test('detects exit when mouse goes away', () {
|
||||||
final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
|
final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[
|
||||||
onEnter: (PointerEnterEvent event) {},
|
_pointerData(PointerChange.hover, const Offset(0.0, 0.0)),
|
||||||
);
|
_pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
|
||||||
int finderCalled = 0;
|
]);
|
||||||
_setUpMouseAnnotationFinder((Offset position) sync* {
|
final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[
|
||||||
finderCalled++;
|
_pointerData(PointerChange.remove, const Offset(1.0, 201.0)),
|
||||||
// This annotation is never in the region
|
]);
|
||||||
});
|
isInHitRegionOne = true;
|
||||||
|
RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
|
||||||
// When no annotations are attached, hovering should not call finder.
|
ui.window.onPointerDataPacket(packet1);
|
||||||
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
|
ui.window.onPointerDataPacket(packet2);
|
||||||
_pointerData(PointerChange.hover, const Offset(0.0, 101.0)),
|
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)),
|
||||||
]));
|
]));
|
||||||
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('should trigger callbacks between parents and children in correct order', () {
|
test('handles mouse down and move', () {
|
||||||
// This test simulates the scenario of a layer being the child of another.
|
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)),
|
||||||
// |A |
|
]);
|
||||||
// | —————— |
|
final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[
|
||||||
// | |B | |
|
_pointerData(PointerChange.down, const Offset(1.0, 101.0)),
|
||||||
// | —————— |
|
_pointerData(PointerChange.move, const Offset(1.0, 201.0)),
|
||||||
// ———————————
|
]);
|
||||||
|
isInHitRegionOne = true;
|
||||||
bool isInB;
|
RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
|
||||||
final List<String> logs = <String>[];
|
ui.window.onPointerDataPacket(packet1);
|
||||||
final MouseTrackerAnnotation annotationA = MouseTrackerAnnotation(
|
ui.window.onPointerDataPacket(packet2);
|
||||||
onEnter: (PointerEnterEvent event) => logs.add('enterA'),
|
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
|
||||||
onExit: (PointerExitEvent event) => logs.add('exitA'),
|
const PointerEnterEvent(position: Offset(0.0, 0.0), delta: Offset(0.0, 0.0)),
|
||||||
onHover: (PointerHoverEvent event) => logs.add('hoverA'),
|
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)),
|
||||||
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']);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue