Unverified Commit fb0b9823 authored by Tong Mu's avatar Tong Mu Committed by GitHub

Change MouseTracker's interface for clarity. Simplify MouseRegion's implementation. (#64119)

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