mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
* Uses a special cursor value MouseCursor.defer to mark deferring. MouseTrackerAnnotation.cursor is now non-null. The mouseCursor argument of widgets or render objects can be null, which indicates using the default value. * Moves SystemMouseCursors.uncontrolled to MouseCursor.uncontrolled. * Changes how MouseCursor.debugDescription is defined. Previously MouseCursor.toString returns $runtimeType($debugDescription), while now it returns $debugDescription. Implementations of classes are updated, except for the ones of MouseCursor.defer and MouseCursor.uncontrolled are simply "defer" and "uncontrolled".
623 lines
24 KiB
Dart
623 lines
24 KiB
Dart
// 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.
|
|
|
|
import 'dart:collection' show LinkedHashSet;
|
|
import 'dart:ui';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/scheduler.dart';
|
|
|
|
import 'mouse_cursor.dart';
|
|
import 'object.dart';
|
|
|
|
/// Signature for listening to [PointerEnterEvent] events.
|
|
///
|
|
/// Used by [MouseTrackerAnnotation], [MouseRegion] and [RenderMouseRegion].
|
|
typedef PointerEnterEventListener = void Function(PointerEnterEvent event);
|
|
|
|
/// Signature for listening to [PointerExitEvent] events.
|
|
///
|
|
/// Used by [MouseTrackerAnnotation], [MouseRegion] and [RenderMouseRegion].
|
|
typedef PointerExitEventListener = void Function(PointerExitEvent event);
|
|
|
|
/// Signature for listening to [PointerHoverEvent] events.
|
|
///
|
|
/// Used by [MouseTrackerAnnotation], [MouseRegion] and [RenderMouseRegion].
|
|
typedef PointerHoverEventListener = void Function(PointerHoverEvent event);
|
|
|
|
/// The annotation object used to annotate regions that are interested in mouse
|
|
/// movements.
|
|
///
|
|
/// To use an annotation, push it with [AnnotatedRegionLayer] during painting.
|
|
/// The annotation's callbacks or configurations will be used depending on the
|
|
/// relationship between annotations and mouse pointers.
|
|
///
|
|
/// A [RenderObject] who uses this class must not dispose this class in its
|
|
/// `detach`, even if it recreates a new one in `attach`, because the object
|
|
/// might be detached and attached during the same frame during a reparent, and
|
|
/// replacing the `MouseTrackerAnnotation` will cause an unnecessary `onExit` and
|
|
/// `onEnter`.
|
|
///
|
|
/// This class is also the type parameter of the annotation search started by
|
|
/// [BaseMouseTracker].
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [BaseMouseTracker], which uses [MouseTrackerAnnotation].
|
|
class MouseTrackerAnnotation with Diagnosticable {
|
|
/// Creates an immutable [MouseTrackerAnnotation].
|
|
///
|
|
/// All arguments are optional. The [cursor] must not be null.
|
|
const MouseTrackerAnnotation({
|
|
this.onEnter,
|
|
this.onHover,
|
|
this.onExit,
|
|
this.cursor = MouseCursor.defer,
|
|
}) : assert(cursor != null);
|
|
|
|
/// Triggered when a mouse pointer, with or without buttons pressed, has
|
|
/// entered the region.
|
|
///
|
|
/// This callback is triggered when the pointer has started to be contained by
|
|
/// the region, either due to a pointer event, or due to the movement or
|
|
/// disappearance of the region. This method is always matched by a later
|
|
/// [onExit].
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [onExit], which is triggered when a mouse pointer exits the region.
|
|
/// * [MouseRegion.onEnter], which uses this callback.
|
|
final PointerEnterEventListener onEnter;
|
|
|
|
/// Triggered when a mouse pointer has moved onto or within the region without
|
|
/// buttons pressed.
|
|
///
|
|
/// This callback is not triggered by the movement of an annotation.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [MouseRegion.onHover], which uses this callback.
|
|
final PointerHoverEventListener onHover;
|
|
|
|
/// Triggered when a mouse pointer, with or without buttons pressed, has
|
|
/// exited the region.
|
|
///
|
|
/// This callback is triggered when the pointer has stopped being contained
|
|
/// by the region, either due to a pointer event, or due to the movement or
|
|
/// disappearance of the region. This method always matches an earlier
|
|
/// [onEnter].
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [onEnter], which is triggered when a mouse pointer enters the region.
|
|
/// * [MouseRegion.onExit], which uses this callback, but is not triggered in
|
|
/// certain cases and does not always match its earier [MouseRegion.onEnter].
|
|
final PointerExitEventListener onExit;
|
|
|
|
/// The mouse cursor for mouse pointers that are hovering over the region.
|
|
///
|
|
/// When a mouse enters the region, its cursor will be changed to the [cursor].
|
|
/// When the mouse leaves the region, the cursor will be set by the region
|
|
/// found at the new location.
|
|
///
|
|
/// Defaults to [MouseCursor.defer], deferring the choice of cursor to the next
|
|
/// region behing it in hit-test order.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [MouseRegion.cursor], which provide values to this field.
|
|
final MouseCursor cursor;
|
|
|
|
@override
|
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
super.debugFillProperties(properties);
|
|
properties.add(FlagsSummary<Function>(
|
|
'callbacks',
|
|
<String, Function> {
|
|
'enter': onEnter,
|
|
'hover': onHover,
|
|
'exit': onExit,
|
|
},
|
|
ifEmpty: '<none>',
|
|
));
|
|
properties.add(DiagnosticsProperty<MouseCursor>('cursor', cursor, defaultValue: MouseCursor.defer));
|
|
}
|
|
}
|
|
|
|
/// Signature for searching for [MouseTrackerAnnotation]s at the given offset.
|
|
///
|
|
/// It is used by the [BaseMouseTracker] to fetch annotations for the mouse
|
|
/// position.
|
|
typedef MouseDetectorAnnotationFinder = Iterable<MouseTrackerAnnotation> Function(Offset offset);
|
|
|
|
// Various states of a connected mouse device used by [BaseMouseTracker].
|
|
class _MouseState {
|
|
_MouseState({
|
|
@required PointerEvent initialEvent,
|
|
}) : assert(initialEvent != null),
|
|
_latestEvent = initialEvent;
|
|
|
|
// The list of annotations that contains this device.
|
|
//
|
|
// It uses [LinkedHashSet] to keep the insertion order.
|
|
LinkedHashSet<MouseTrackerAnnotation> get annotations => _annotations;
|
|
LinkedHashSet<MouseTrackerAnnotation> _annotations = LinkedHashSet<MouseTrackerAnnotation>();
|
|
|
|
LinkedHashSet<MouseTrackerAnnotation> replaceAnnotations(LinkedHashSet<MouseTrackerAnnotation> value) {
|
|
assert(value != null);
|
|
final LinkedHashSet<MouseTrackerAnnotation> previous = _annotations;
|
|
_annotations = value;
|
|
return previous;
|
|
}
|
|
|
|
// The most recently processed mouse event observed from this device.
|
|
PointerEvent get latestEvent => _latestEvent;
|
|
PointerEvent _latestEvent;
|
|
|
|
PointerEvent replaceLatestEvent(PointerEvent value) {
|
|
assert(value != null);
|
|
assert(value.device == _latestEvent.device);
|
|
final PointerEvent previous = _latestEvent;
|
|
_latestEvent = value;
|
|
return previous;
|
|
}
|
|
|
|
int get device => latestEvent.device;
|
|
|
|
@override
|
|
String toString() {
|
|
String describeEvent(PointerEvent event) {
|
|
return event == null ? 'null' : describeIdentity(event);
|
|
}
|
|
final String describeLatestEvent = 'latestEvent: ${describeEvent(latestEvent)}';
|
|
final String describeAnnotations = 'annotations: [list of ${annotations.length}]';
|
|
return '${describeIdentity(this)}($describeLatestEvent, $describeAnnotations)';
|
|
}
|
|
}
|
|
|
|
/// Used by [BaseMouseTracker] to provide the details of an update of a mouse
|
|
/// device.
|
|
///
|
|
/// This class contains the information needed to handle the update that might
|
|
/// change the state of a mouse device, or the [MouseTrackerAnnotation]s that
|
|
/// the mouse device is hovering.
|
|
@immutable
|
|
class MouseTrackerUpdateDetails with Diagnosticable {
|
|
/// When device update is triggered by a new frame.
|
|
///
|
|
/// All parameters are required.
|
|
const MouseTrackerUpdateDetails.byNewFrame({
|
|
@required this.lastAnnotations,
|
|
@required this.nextAnnotations,
|
|
@required this.previousEvent,
|
|
}) : assert(previousEvent != null),
|
|
assert(lastAnnotations != null),
|
|
assert(nextAnnotations != null),
|
|
triggeringEvent = null;
|
|
|
|
/// When device update is triggered by a pointer event.
|
|
///
|
|
/// The [lastAnnotations], [nextAnnotations], and [triggeringEvent] are
|
|
/// required.
|
|
const MouseTrackerUpdateDetails.byPointerEvent({
|
|
@required this.lastAnnotations,
|
|
@required this.nextAnnotations,
|
|
this.previousEvent,
|
|
@required this.triggeringEvent,
|
|
}) : assert(triggeringEvent != null),
|
|
assert(lastAnnotations != null),
|
|
assert(nextAnnotations != null);
|
|
|
|
/// The annotations that the device is hovering before the update.
|
|
///
|
|
/// It is never null.
|
|
final LinkedHashSet<MouseTrackerAnnotation> lastAnnotations;
|
|
|
|
/// The annotations that the device is hovering after the update.
|
|
///
|
|
/// It is never null.
|
|
final LinkedHashSet<MouseTrackerAnnotation> nextAnnotations;
|
|
|
|
/// The last event that the device observed before the update.
|
|
///
|
|
/// If the update is triggered by a frame, the [previousEvent] is never null,
|
|
/// since the pointer must have been added before.
|
|
///
|
|
/// If the update is triggered by a pointer event, the [previousEvent] is not
|
|
/// null except for cases where the event is the first event observed by the
|
|
/// pointer (which is not necessarily a [PointerAddedEvent]).
|
|
final PointerEvent previousEvent;
|
|
|
|
/// The event that triggered this update.
|
|
///
|
|
/// It is non-null if and only if the update is triggered by a pointer event.
|
|
final PointerEvent triggeringEvent;
|
|
|
|
/// The pointing device of this update.
|
|
int get device {
|
|
final int result = (previousEvent ?? triggeringEvent).device;
|
|
assert(result != null);
|
|
return result;
|
|
}
|
|
|
|
/// The last event that the device observed after the update.
|
|
///
|
|
/// The [latestEvent] is never null.
|
|
PointerEvent get latestEvent {
|
|
final PointerEvent result = triggeringEvent ?? previousEvent;
|
|
assert(result != null);
|
|
return result;
|
|
}
|
|
|
|
@override
|
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
super.debugFillProperties(properties);
|
|
properties.add(IntProperty('device', device));
|
|
properties.add(DiagnosticsProperty<PointerEvent>('previousEvent', previousEvent));
|
|
properties.add(DiagnosticsProperty<PointerEvent>('triggeringEvent', triggeringEvent));
|
|
properties.add(DiagnosticsProperty<Set<MouseTrackerAnnotation>>('lastAnnotations', lastAnnotations));
|
|
properties.add(DiagnosticsProperty<Set<MouseTrackerAnnotation>>('nextAnnotations', nextAnnotations));
|
|
}
|
|
}
|
|
|
|
/// 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.
|
|
///
|
|
/// 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.
|
|
/// * [MouseCursorMixin], 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();
|
|
});
|
|
}
|
|
}
|
|
|
|
/// 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.
|
|
final Map<int, _MouseState> _mouseStates = <int, _MouseState>{};
|
|
|
|
// Used to wrap any procedure that might change `mouseIsConnected`.
|
|
//
|
|
// This method records `mouseIsConnected`, runs `task`, and calls
|
|
// [notifyListeners] at the end if the `mouseIsConnected` has changed.
|
|
void _monitorMouseConnection(VoidCallback task) {
|
|
final bool mouseWasConnected = mouseIsConnected;
|
|
task();
|
|
if (mouseWasConnected != mouseIsConnected)
|
|
notifyListeners();
|
|
}
|
|
|
|
bool _debugDuringDeviceUpdate = false;
|
|
// Used to wrap any procedure that might call [handleDeviceUpdate].
|
|
//
|
|
// In debug mode, this method uses `_debugDuringDeviceUpdate` to prevent
|
|
// `_deviceUpdatePhase` being recursively called.
|
|
void _deviceUpdatePhase(VoidCallback task) {
|
|
assert(!_debugDuringDeviceUpdate);
|
|
assert(() {
|
|
_debugDuringDeviceUpdate = true;
|
|
return true;
|
|
}());
|
|
task();
|
|
assert(() {
|
|
_debugDuringDeviceUpdate = false;
|
|
return true;
|
|
}());
|
|
}
|
|
|
|
// Whether an observed event might update a device.
|
|
static bool _shouldMarkStateDirty(_MouseState state, PointerEvent event) {
|
|
if (state == null)
|
|
return true;
|
|
assert(event != null);
|
|
final PointerEvent lastEvent = state.latestEvent;
|
|
assert(event.device == lastEvent.device);
|
|
// An Added can only follow a Removed, and a Removed can only be followed
|
|
// by an Added.
|
|
assert((event is PointerAddedEvent) == (lastEvent is PointerRemovedEvent));
|
|
|
|
// Ignore events that are unrelated to mouse tracking.
|
|
if (event is PointerSignalEvent)
|
|
return false;
|
|
return lastEvent is PointerAddedEvent
|
|
|| event is PointerRemovedEvent
|
|
|| lastEvent.position != event.position;
|
|
}
|
|
|
|
// Find the annotations that is hovered by the device of the `state`.
|
|
//
|
|
// If the device is not connected, an empty set is returned without calling
|
|
// `annotationFinder`.
|
|
LinkedHashSet<MouseTrackerAnnotation> _findAnnotations(_MouseState state) {
|
|
final Offset globalPosition = state.latestEvent.position;
|
|
final int device = state.device;
|
|
return (_mouseStates.containsKey(device))
|
|
? LinkedHashSet<MouseTrackerAnnotation>.from(annotationFinder(globalPosition))
|
|
: <MouseTrackerAnnotation>{} as LinkedHashSet<MouseTrackerAnnotation>;
|
|
}
|
|
|
|
/// A callback that is called on the update of a device.
|
|
///
|
|
/// This method should be called only by [BaseMouseTracker].
|
|
///
|
|
/// 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,
|
|
///
|
|
/// The update can be caused by two kinds of triggers:
|
|
///
|
|
/// * Triggered by the addition, movement, or removal of a pointer. Such
|
|
/// calls occur during the handler of the event, indicated by
|
|
/// `details.triggeringEvent` being non-null.
|
|
/// * Triggered by the appearance, movement, or disappearance of an annotation.
|
|
/// 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
|
|
void handleDeviceUpdate(MouseTrackerUpdateDetails details) {
|
|
assert(_debugDuringDeviceUpdate);
|
|
}
|
|
|
|
// Handler for events coming from the PointerRouter.
|
|
//
|
|
// If the event marks the device dirty, update the device immediately.
|
|
void _handleEvent(PointerEvent event) {
|
|
if (event.kind != PointerDeviceKind.mouse)
|
|
return;
|
|
if (event is PointerSignalEvent)
|
|
return;
|
|
final int device = event.device;
|
|
final _MouseState existingState = _mouseStates[device];
|
|
if (!_shouldMarkStateDirty(existingState, event))
|
|
return;
|
|
|
|
_monitorMouseConnection(() {
|
|
_deviceUpdatePhase(() {
|
|
// Update mouseState to the latest devices that have not been removed,
|
|
// so that [mouseIsConnected], which is decided by `_mouseStates`, is
|
|
// correct during the callbacks.
|
|
if (existingState == null) {
|
|
_mouseStates[device] = _MouseState(initialEvent: event);
|
|
} else {
|
|
assert(event is! PointerAddedEvent);
|
|
if (event is PointerRemovedEvent)
|
|
_mouseStates.remove(event.device);
|
|
}
|
|
final _MouseState targetState = _mouseStates[device] ?? existingState;
|
|
|
|
final PointerEvent lastEvent = targetState.replaceLatestEvent(event);
|
|
final LinkedHashSet<MouseTrackerAnnotation> nextAnnotations = _findAnnotations(targetState);
|
|
final LinkedHashSet<MouseTrackerAnnotation> lastAnnotations = targetState.replaceAnnotations(nextAnnotations);
|
|
|
|
handleDeviceUpdate(MouseTrackerUpdateDetails.byPointerEvent(
|
|
lastAnnotations: lastAnnotations,
|
|
nextAnnotations: nextAnnotations,
|
|
previousEvent: lastEvent,
|
|
triggeringEvent: event,
|
|
));
|
|
});
|
|
});
|
|
}
|
|
|
|
// 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() {
|
|
_deviceUpdatePhase(() {
|
|
for (final _MouseState dirtyState in _mouseStates.values) {
|
|
final PointerEvent lastEvent = dirtyState.latestEvent;
|
|
final LinkedHashSet<MouseTrackerAnnotation> nextAnnotations = _findAnnotations(dirtyState);
|
|
final LinkedHashSet<MouseTrackerAnnotation> lastAnnotations = dirtyState.replaceAnnotations(nextAnnotations);
|
|
|
|
handleDeviceUpdate(MouseTrackerUpdateDetails.byNewFrame(
|
|
lastAnnotations: lastAnnotations,
|
|
nextAnnotations: nextAnnotations,
|
|
previousEvent: lastEvent,
|
|
));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// A mixin for [BaseMouseTracker] that dispatches mouse events on device update.
|
|
//
|
|
// See also:
|
|
//
|
|
// * [MouseTracker], which uses this mixin.
|
|
mixin _MouseTrackerEventMixin on BaseMouseTracker {
|
|
// Handles device update and dispatches mouse event callbacks.
|
|
static void _handleDeviceUpdateMouseEvents(MouseTrackerUpdateDetails details) {
|
|
final PointerEvent previousEvent = details.previousEvent;
|
|
final PointerEvent triggeringEvent = details.triggeringEvent;
|
|
final PointerEvent latestEvent = details.latestEvent;
|
|
|
|
final LinkedHashSet<MouseTrackerAnnotation> lastAnnotations = details.lastAnnotations;
|
|
final LinkedHashSet<MouseTrackerAnnotation> nextAnnotations = details.nextAnnotations;
|
|
|
|
// Order is important for mouse event callbacks. The `findAnnotations`
|
|
// returns annotations in the visual order from front to back. We call
|
|
// it the "visual order", and the opposite one "reverse visual order".
|
|
// The algorithm here is explained in
|
|
// https://github.com/flutter/flutter/issues/41420
|
|
|
|
// Send exit events to annotations that are in last but not in next, in
|
|
// visual order.
|
|
final Iterable<MouseTrackerAnnotation> exitingAnnotations = lastAnnotations.difference(nextAnnotations);
|
|
for (final MouseTrackerAnnotation annotation in exitingAnnotations) {
|
|
if (annotation.onExit != null)
|
|
annotation.onExit(PointerExitEvent.fromMouseEvent(latestEvent));
|
|
}
|
|
|
|
// Send enter events to annotations that are not in last but in next, in
|
|
// reverse visual order.
|
|
final Iterable<MouseTrackerAnnotation> enteringAnnotations =
|
|
nextAnnotations.difference(lastAnnotations).toList().reversed;
|
|
for (final MouseTrackerAnnotation annotation in enteringAnnotations) {
|
|
if (annotation.onEnter != null)
|
|
annotation.onEnter(PointerEnterEvent.fromMouseEvent(latestEvent));
|
|
}
|
|
|
|
// Send hover events to annotations that are in next, in reverse visual
|
|
// order. The reverse visual order is chosen only because of the simplicity
|
|
// by keeping the hover events aligned with enter events.
|
|
if (triggeringEvent is PointerHoverEvent) {
|
|
final Offset hoverPositionBeforeUpdate = previousEvent is PointerHoverEvent ? previousEvent.position : null;
|
|
final bool pointerHasMoved = hoverPositionBeforeUpdate == null || hoverPositionBeforeUpdate != triggeringEvent.position;
|
|
// If the hover event follows a non-hover event, or has moved since the
|
|
// last hover, then trigger the hover callback on all annotations.
|
|
// Otherwise, trigger the hover callback only on annotations that it
|
|
// newly enters.
|
|
final Iterable<MouseTrackerAnnotation> hoveringAnnotations = pointerHasMoved ? nextAnnotations.toList().reversed : enteringAnnotations;
|
|
for (final MouseTrackerAnnotation annotation in hoveringAnnotations) {
|
|
if (annotation.onHover != null) {
|
|
annotation.onHover(triggeringEvent);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@protected
|
|
@override
|
|
void handleDeviceUpdate(MouseTrackerUpdateDetails details) {
|
|
super.handleDeviceUpdate(details);
|
|
_handleDeviceUpdateMouseEvents(details);
|
|
}
|
|
}
|
|
|
|
/// Trackes the relationship between mouse devices and annotations, and
|
|
/// triggers mouse events and cursor changes accordingly.
|
|
///
|
|
/// The [MouseTracker] trackes the relationship between mouse devices and
|
|
/// [MouseTrackerAnnotation]s, and when such relationship changes, triggers
|
|
/// the following changes if applicable:
|
|
///
|
|
/// * Dispatches mouse-related pointer events (pointer enter, hover, and exit).
|
|
/// * Notifies changes of [mouseIsConnected].
|
|
/// * Changes mouse cursors.
|
|
///
|
|
/// An instance of [MouseTracker] is owned by the global singleton of
|
|
/// [RendererBinding].
|
|
///
|
|
/// This class is a [ChangeNotifier] that notifies its listeners if the value of
|
|
/// [mouseIsConnected] changes.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [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);
|
|
}
|