blob: 94cc5833e8c82f9bd47750ba57fc6ba10c40d1ad [file] [log] [blame]
// Copyright 2018 The Chromium 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/scheduler.dart';
import 'events.dart';
import 'pointer_router.dart';
/// Signature for listening to [PointerEnterEvent] events.
///
/// Used by [MouseTrackerAnnotation], [Listener] and [RenderPointerListener].
typedef PointerEnterEventListener = void Function(PointerEnterEvent event);
/// Signature for listening to [PointerExitEvent] events.
///
/// Used by [MouseTrackerAnnotation], [Listener] and [RenderPointerListener].
typedef PointerExitEventListener = void Function(PointerExitEvent event);
/// Signature for listening to [PointerHoverEvent] events.
///
/// Used by [MouseTrackerAnnotation], [Listener] and [RenderPointerListener].
typedef PointerHoverEventListener = void Function(PointerHoverEvent event);
/// The annotation object used to annotate layers that are interested in mouse
/// movements.
///
/// This is added to a layer and managed by the [Listener] widget.
class MouseTrackerAnnotation {
/// Creates an annotation that can be used to find layers interested in mouse
/// movements.
const MouseTrackerAnnotation({this.onEnter, this.onHover, this.onExit});
/// Triggered when a pointer has entered the bounding box of the annotated
/// layer.
final PointerEnterEventListener onEnter;
/// Triggered when a pointer has moved within the bounding box of the
/// annotated layer.
final PointerHoverEventListener onHover;
/// Triggered when a pointer has exited the bounding box of the annotated
/// layer.
final PointerExitEventListener onExit;
@override
String toString() {
final List<String> callbacks = <String>[];
if (onEnter != null)
callbacks.add('enter');
if (onHover != null)
callbacks.add('hover');
if (onExit != null)
callbacks.add('exit');
final String describeCallbacks = callbacks.isEmpty
? '<none>'
: callbacks.join(' ');
return '${describeIdentity(this)}(callbacks: $describeCallbacks)';
}
}
/// Signature for searching for [MouseTrackerAnnotation]s at the given offset.
///
/// It is used by the [MouseTracker] to fetch annotations for the mouse
/// position.
typedef MouseDetectorAnnotationFinder = Iterable<MouseTrackerAnnotation> Function(Offset offset);
// Various states of each connected mouse device.
//
// It is used by [MouseTracker] to compute which callbacks should be triggered
// by each event.
class _MouseState {
_MouseState({
@required PointerEvent mostRecentEvent,
}) : assert(mostRecentEvent != null),
_mostRecentEvent = mostRecentEvent;
// The list of annotations that contains this device during the last frame.
//
// It uses [LinkedHashSet] to keep the insertion order.
LinkedHashSet<MouseTrackerAnnotation> lastAnnotations = LinkedHashSet<MouseTrackerAnnotation>();
// The most recent mouse event observed from this device.
//
// The [mostRecentEvent] is never null.
PointerEvent get mostRecentEvent => _mostRecentEvent;
PointerEvent _mostRecentEvent;
set mostRecentEvent(PointerEvent value) {
assert(value != null);
assert(value.device == _mostRecentEvent.device);
_mostRecentEvent = value;
}
int get device => _mostRecentEvent.device;
@override
String toString() {
final String describeEvent = '${_mostRecentEvent.runtimeType}(device: ${_mostRecentEvent.device})';
final String describeAnnotations = '[list of ${lastAnnotations.length}]';
return '${describeIdentity(this)}(event: $describeEvent, annotations: $describeAnnotations)';
}
}
/// Maintains the relationship between mouse devices and
/// [MouseTrackerAnnotation]s, and notifies interested callbacks of the changes
/// thereof.
///
/// This class is a [ChangeNotifier] that notifies its listeners if the value of
/// [mouseIsConnected] changes.
///
/// An instance of [MouseTracker] is owned by the global singleton of
/// [RendererBinding].
class MouseTracker extends ChangeNotifier {
/// Creates a mouse tracker to keep track of mouse locations.
///
/// The first parameter is a [PointerRouter], which [MouseTracker] will
/// subscribe to and receive events from. Usually it is the global singleton
/// instance [GestureBinding.pointerRouter].
///
/// The second parameter is a function with which the [MouseTracker] can
/// search for [MouseTrackerAnnotation]s at a given position.
/// Usually it is [Layer.findAllAnnotations] of the root layer.
///
/// All of the parameters must not be null.
MouseTracker(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.
///
/// [MouseTracker] uses this callback to know which annotations are affected
/// by each device.
///
/// The annotations should be returned in visual order from front to
/// back, so that the callbacks are called in an correct order.
final MouseDetectorAnnotationFinder annotationFinder;
// The pointer router that the mouse tracker listens to, and receives new
// mouse events from.
final PointerRouter _router;
// 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>{};
// Returns the mouse state of a device. If it doesn't exist, create one using
// `mostRecentEvent`.
//
// The returned value is never null.
_MouseState _guaranteeMouseState(int device, PointerEvent mostRecentEvent) {
final _MouseState currentState = _mouseStates[device];
if (currentState == null) {
_addMouseDevice(device, mostRecentEvent);
}
final _MouseState result = currentState ?? _mouseStates[device];
assert(result != null);
return result;
}
// The collection of annotations that are currently being tracked.
// It is operated on by [attachAnnotation] and [detachAnnotation].
final Set<MouseTrackerAnnotation> _trackedAnnotations = <MouseTrackerAnnotation>{};
bool get _hasAttachedAnnotations => _trackedAnnotations.isNotEmpty;
void _addMouseDevice(int device, PointerEvent event) {
final bool wasConnected = mouseIsConnected;
assert(!_mouseStates.containsKey(device));
_mouseStates[device] = _MouseState(mostRecentEvent: event);
// Schedule a check to enter annotations that might contain this pointer.
_checkDeviceUpdates(device: device);
if (mouseIsConnected != wasConnected) {
notifyListeners();
}
}
void _removeMouseDevice(int device, PointerEvent event) {
final bool wasConnected = mouseIsConnected;
assert(_mouseStates.containsKey(device));
final _MouseState disconnectedMouseState = _mouseStates.remove(device);
disconnectedMouseState.mostRecentEvent = event;
// Schedule a check to exit annotations that used to contain this pointer.
_checkDeviceUpdates(
device: device,
disconnectedMouseState: disconnectedMouseState,
);
if (mouseIsConnected != wasConnected) {
notifyListeners();
}
}
// Handler for events coming from the PointerRouter.
void _handleEvent(PointerEvent event) {
if (event.kind != PointerDeviceKind.mouse) {
return;
}
final int device = event.device;
if (event is PointerAddedEvent) {
_addMouseDevice(device, event);
} else if (event is PointerRemovedEvent) {
_removeMouseDevice(device, event);
} else if (event is PointerHoverEvent) {
final _MouseState mouseState = _guaranteeMouseState(device, event);
final PointerEvent previousEvent = mouseState.mostRecentEvent;
mouseState.mostRecentEvent = event;
if (previousEvent is PointerAddedEvent || previousEvent.position != event.position) {
// Only send notifications if we have our first event, or if the
// location of the mouse has changed
_checkDeviceUpdates(device: device);
}
}
}
bool _scheduledPostFramePositionCheck = false;
// Schedules a position check at the end of this frame.
// It is only called during a frame during which annotations have been added.
void _scheduleMousePositionCheck() {
// If we're not tracking anything, then there is no point in registering a
// frame callback or scheduling a frame. By definition there are no active
// annotations that need exiting, either.
if (!_scheduledPostFramePositionCheck) {
_scheduledPostFramePositionCheck = true;
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
_checkAllDevicesUpdates();
_scheduledPostFramePositionCheck = false;
});
}
}
// Collect the latest states of the given mouse device `device`, and call
// interested callbacks.
//
// The enter or exit events are called for annotations that the pointer
// enters or leaves, while hover events are always called for each
// annotations that the pointer stays in, even if the pointer has not moved
// since the last call. Therefore it's caller's responsibility to check if
// the pointer has moved.
//
// If `disconnectedMouseState` is provided, this state will be used instead,
// but this mouse will be hovering no annotations.
void _checkDeviceUpdates({
int device,
_MouseState disconnectedMouseState,
}) {
final _MouseState mouseState = disconnectedMouseState ?? _mouseStates[device];
final bool thisDeviceIsConnected = mouseState != disconnectedMouseState;
assert(mouseState != null);
final LinkedHashSet<MouseTrackerAnnotation> nextAnnotations =
(_hasAttachedAnnotations && thisDeviceIsConnected)
? LinkedHashSet<MouseTrackerAnnotation>.from(
annotationFinder(mouseState.mostRecentEvent.position)
)
: <MouseTrackerAnnotation>{};
_dispatchDeviceCallbacks(
currentState: mouseState,
nextAnnotations: nextAnnotations,
);
mouseState.lastAnnotations = nextAnnotations;
}
// Collect the latest states of all mouse devices, and call interested
// callbacks.
//
// For detailed behaviors, see [_checkDeviceUpdates].
void _checkAllDevicesUpdates() {
for (final int device in _mouseStates.keys) {
_checkDeviceUpdates(device: device);
}
}
// Dispatch callbacks related to a device after all necessary information
// has been collected.
//
// This function should not change the provided states, and should not access
// information that is not provided in parameters (hence being static).
static void _dispatchDeviceCallbacks({
@required LinkedHashSet<MouseTrackerAnnotation> nextAnnotations,
@required _MouseState currentState,
}) {
// Order is important for mouse event callbacks. The `findAnnotations`
// returns annotations in the visual order from front to back. We call
// it the "visual order", and the opposite one "reverse visual order".
// The algorithm here is explained in
// https://github.com/flutter/flutter/issues/41420
// The `nextAnnotations` is annotations that contains this device in the
// coming frame in visual order.
// Order is preserved with the help of [LinkedHashSet].
final PointerEvent mostRecentEvent = currentState.mostRecentEvent;
// The `lastAnnotations` is annotations that contains this device in the
// previous frame in visual order.
final LinkedHashSet<MouseTrackerAnnotation> lastAnnotations = currentState.lastAnnotations;
// Send exit events in visual order.
final Iterable<MouseTrackerAnnotation> exitingAnnotations =
lastAnnotations.difference(nextAnnotations);
for (final MouseTrackerAnnotation annotation in exitingAnnotations) {
if (annotation.onExit != null) {
annotation.onExit(PointerExitEvent.fromMouseEvent(mostRecentEvent));
}
}
// Send enter events in reverse visual order.
final Iterable<MouseTrackerAnnotation> enteringAnnotations =
nextAnnotations.difference(lastAnnotations).toList().reversed;
for (final MouseTrackerAnnotation annotation in enteringAnnotations) {
if (annotation.onEnter != null) {
annotation.onEnter(PointerEnterEvent.fromMouseEvent(mostRecentEvent));
}
}
// Send hover events in reverse visual order.
// For now the order between the hover events is designed this way for no
// solid reasons but to keep it aligned with enter events for simplicity.
if (mostRecentEvent is PointerHoverEvent) {
final Iterable<MouseTrackerAnnotation> hoveringAnnotations =
nextAnnotations.toList().reversed;
for (final MouseTrackerAnnotation annotation in hoveringAnnotations) {
if (annotation.onHover != null) {
annotation.onHover(mostRecentEvent);
}
}
}
}
/// Checks if the given [MouseTrackerAnnotation] is attached to this
/// [MouseTracker].
///
/// This function is only public to allow for proper testing of the
/// MouseTracker. Do not call in other contexts.
@visibleForTesting
bool isAnnotationAttached(MouseTrackerAnnotation annotation) {
return _trackedAnnotations.contains(annotation);
}
/// Whether or not a mouse is connected and has produced events.
bool get mouseIsConnected => _mouseStates.isNotEmpty;
/// Notify [MouseTracker] that a new mouse tracker annotation has started to
/// take effect.
///
/// This should be called as soon as the layer that owns this annotation is
/// added to the layer tree.
///
/// This triggers [MouseTracker] to schedule a mouse position check during the
/// post frame to see if this new annotation might trigger enter events.
///
/// The [MouseTracker] also uses this to track the number of attached
/// annotations, and will skip mouse position checks if there is no
/// annotations attached.
void attachAnnotation(MouseTrackerAnnotation annotation) {
// Schedule a check so that we test this new annotation to see if any mouse
// is currently inside its region. It has to happen after the frame is
// complete so that the annotation layer has been added before the check.
_trackedAnnotations.add(annotation);
if (mouseIsConnected) {
_scheduleMousePositionCheck();
}
}
/// Notify [MouseTracker] that a mouse tracker annotation that was previously
/// attached has stopped taking effect.
///
/// This should be called as soon as the layer that owns this annotation is
/// removed from the layer tree. An assertion error will be thrown if the
/// associated layer is not removed and receives another mouse hit.
///
/// This triggers [MouseTracker] to perform a mouse position check immediately
/// to see if this annotation removal triggers any exit events.
///
/// The [MouseTracker] also uses this to track the number of attached
/// annotations, and will skip mouse position checks if there is no
/// annotations attached.
void detachAnnotation(MouseTrackerAnnotation annotation) {
_mouseStates.forEach((int device, _MouseState mouseState) {
if (mouseState.lastAnnotations.contains(annotation)) {
if (annotation.onExit != null) {
final PointerEvent event = mouseState.mostRecentEvent;
assert(event != null);
annotation.onExit(PointerExitEvent.fromMouseEvent(event));
}
mouseState.lastAnnotations.remove(annotation);
}
});
_trackedAnnotations.remove(annotation);
}
}