mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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.
This commit is contained in:
parent
1fc3a5e439
commit
fb0b982324
@ -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();
|
||||
|
||||
|
||||
107
packages/flutter/test/rendering/mouse_tracking_test_utils.dart
Normal file
107
packages/flutter/test/rendering/mouse_tracking_test_utils.dart
Normal file
@ -0,0 +1,107 @@
|
||||
// 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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user