blob: 9f16a89a2580b14f2acc8cafdca24dc3d282a005 [file] [log] [blame]
// 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';
/// 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 layers that are interested in mouse
/// movements.
///
/// This is added to a layer and managed by the [MouseRegion] widget.
class MouseTrackerAnnotation with Diagnosticable {
/// 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 mouse pointer, with or without buttons pressed, has
/// entered the annotated region.
///
/// This callback is triggered when the pointer has started to be contained
/// by the annotationed region for any reason, which means it always matches 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 pointer has moved within the annotated region without
/// buttons pressed.
///
/// This callback is triggered when:
///
/// * An annotation that did not contain the pointer has moved to under a
/// pointer that has no buttons pressed.
/// * A pointer has moved onto, or moved within an annotation without buttons
/// pressed.
///
/// This callback is not triggered when:
///
/// * An annotation that is containing the pointer has moved, and still
/// contains the pointer.
final PointerHoverEventListener onHover;
/// Triggered when a mouse pointer, with or without buttons pressed, has
/// exited the annotated region when the annotated region still exists.
///
/// This callback is triggered when the pointer has stopped being contained
/// by the region for any reason, which means it always matches an earlier
/// [onEnter].
///
/// See also:
///
/// * [onEnter], which is triggered when a mouse pointer enters the region.
/// * [RenderMouseRegion.onExit], which uses this callback.
/// * [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;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(FlagsSummary<Function>(
'callbacks',
<String, Function> {
'enter': onEnter,
'hover': onHover,
'exit': onExit,
},
ifEmpty: '<none>',
));
}
}
/// 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);
typedef _UpdatedDeviceHandler = void Function(_MouseState mouseState, LinkedHashSet<MouseTrackerAnnotation> previousAnnotations);
// Various states of a connected mouse device used by [MouseTracker].
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) {
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;
set latestEvent(PointerEvent value) {
assert(value != null);
_latestEvent = value;
}
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)';
}
}
/// 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].
///
/// ### Details
///
/// The state of [MouseTracker] 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.
/// * A frame has been painted. In this case, a callback will be scheduled for
/// the upcoming post-frame phase to update all devices.
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>{};
// Whether an observed event might update a device.
static bool _shouldMarkStateDirty(_MouseState state, PointerEvent value) {
if (state == null)
return true;
assert(value != null);
final PointerEvent lastEvent = state.latestEvent;
assert(value.device == lastEvent.device);
// An Added can only follow a Removed, and a Removed can only be followed
// by an Added.
assert((value is PointerAddedEvent) == (lastEvent is PointerRemovedEvent));
// Ignore events that are unrelated to mouse tracking.
if (value is PointerSignalEvent)
return false;
return lastEvent is PointerAddedEvent
|| value is PointerRemovedEvent
|| lastEvent.position != value.position;
}
// 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;
final PointerEvent previousEvent = existingState?.latestEvent;
_updateDevices(
targetEvent: event,
handleUpdatedDevice: (_MouseState mouseState, LinkedHashSet<MouseTrackerAnnotation> previousAnnotations) {
assert(mouseState.device == event.device);
_dispatchDeviceCallbacks(
lastAnnotations: previousAnnotations,
nextAnnotations: mouseState.annotations,
previousEvent: previousEvent,
unhandledEvent: event,
);
},
);
}
// 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>;
}
static bool get _duringBuildPhase {
return SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks;
}
// 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() {
_updateDevices(
handleUpdatedDevice: (_MouseState mouseState, LinkedHashSet<MouseTrackerAnnotation> previousAnnotations) {
_dispatchDeviceCallbacks(
lastAnnotations: previousAnnotations,
nextAnnotations: mouseState.annotations,
previousEvent: mouseState.latestEvent,
unhandledEvent: null,
);
}
);
}
bool _duringDeviceUpdate = false;
// Update device states with the change of a new event or a new frame, and
// trigger `handleUpdateDevice` for each dirty device.
//
// This method is called either when a new event is observed (`targetEvent`
// being non-null), or when no new event is observed but all devices are
// marked dirty due to a new frame. It means that it will not happen that all
// devices are marked dirty when a new event is unprocessed.
//
// This method is the moment where `_mouseState` is updated. Before
// this method, `_mouseState` is in sync with the state before the event or
// before the frame. During `handleUpdateDevice` and after this method,
// `_mouseState` is in sync with the state after the event or after the frame.
//
// The dirty devices are decided as follows: if `targetEvent` is not null, the
// dirty devices are the device that observed the event; otherwise all devices
// are dirty.
//
// This method first keeps `_mouseStates` up to date. More specifically,
//
// * If an event is observed, update `_mouseStates` by inserting or removing
// the state that corresponds to the event if needed, then update the
// `latestEvent` property of this mouse state.
// * For each mouse state that will correspond to a dirty device, update the
// `annotations` property with the annotations the device is contained.
//
// Then, for each dirty device, `handleUpdatedDevice` is called with the
// updated state and the annotations before the update.
//
// Last, the method checks if `mouseIsConnected` has been changed, and notify
// listeners if needed.
void _updateDevices({
PointerEvent targetEvent,
@required _UpdatedDeviceHandler handleUpdatedDevice,
}) {
assert(handleUpdatedDevice != null);
assert(!_duringBuildPhase);
assert(!_duringDeviceUpdate);
final bool mouseWasConnected = mouseIsConnected;
// If new event is not null, only the device that observed this event is
// dirty. The target device's state is inserted into or removed from
// `_mouseStates` if needed, stored as `targetState`, and its
// `mostRecentDevice` is updated.
_MouseState targetState;
if (targetEvent != null) {
targetState = _mouseStates[targetEvent.device];
if (targetState == null) {
targetState = _MouseState(initialEvent: targetEvent);
_mouseStates[targetState.device] = targetState;
} else {
assert(targetEvent is! PointerAddedEvent);
targetState.latestEvent = targetEvent;
// 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 (targetEvent is PointerRemovedEvent)
_mouseStates.remove(targetEvent.device);
}
}
assert((targetState == null) == (targetEvent == null));
assert(() {
_duringDeviceUpdate = true;
return true;
}());
// We can safely use `_mouseStates` here without worrying about the removed
// state, because `targetEvent` should be null when `_mouseStates` is used.
final Iterable<_MouseState> dirtyStates = targetEvent == null ? _mouseStates.values : <_MouseState>[targetState];
for (final _MouseState dirtyState in dirtyStates) {
final LinkedHashSet<MouseTrackerAnnotation> nextAnnotations = _findAnnotations(dirtyState);
final LinkedHashSet<MouseTrackerAnnotation> lastAnnotations = dirtyState.replaceAnnotations(nextAnnotations);
handleUpdatedDevice(dirtyState, lastAnnotations);
}
assert(() {
_duringDeviceUpdate = false;
return true;
}());
if (mouseWasConnected != mouseIsConnected)
notifyListeners();
}
// Dispatch callbacks related to a device after all necessary information
// has been collected.
//
// The `previousEvent` is the latest event before `unhandledEvent`. It might be
// null, which means the update is triggered by a new event.
// The `unhandledEvent` can be null, which means the update is not triggered
// by an event.
// However, one of `previousEvent` or `unhandledEvent` must not be null.
static void _dispatchDeviceCallbacks({
@required LinkedHashSet<MouseTrackerAnnotation> lastAnnotations,
@required LinkedHashSet<MouseTrackerAnnotation> nextAnnotations,
@required PointerEvent previousEvent,
@required PointerEvent unhandledEvent,
}) {
assert(lastAnnotations != null);
assert(nextAnnotations != null);
final PointerEvent latestEvent = unhandledEvent ?? previousEvent;
assert(latestEvent != null);
// 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.where(
(MouseTrackerAnnotation value) => !nextAnnotations.contains(value),
);
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 (unhandledEvent is PointerHoverEvent) {
final Offset lastHoverPosition = previousEvent is PointerHoverEvent ? previousEvent.position : null;
final bool pointerHasMoved = lastHoverPosition == null || lastHoverPosition != unhandledEvent.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(unhandledEvent);
}
}
}
}
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(_duringBuildPhase);
assert(!_duringDeviceUpdate);
if (!mouseIsConnected)
return;
if (!_hasScheduledPostFrameCheck) {
_hasScheduledPostFrameCheck = true;
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
assert(_hasScheduledPostFrameCheck);
_hasScheduledPostFrameCheck = false;
_updateAllDevices();
});
}
}
/// Whether or not a mouse is connected and has produced events.
bool get mouseIsConnected => _mouseStates.isNotEmpty;
}