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