blob: 022c71a17bf2692173ee8d435207e272f317cd1c [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.
// This library intentionally uses the LinkedHashMap constructor to declare that
// entries will be ordered. Using collection literals for this requires casting the
// resulting map, which has a runtime cost.
// ignore_for_file: prefer_collection_literals
import 'dart:collection' show LinkedHashMap;
import 'package:engine/ui.dart';
import 'package:flute/foundation.dart';
import 'package:flute/gestures.dart';
import 'package:flute/services.dart';
import 'object.dart';
export 'package:flute/services.dart' show
MouseCursor,
SystemMouseCursors;
/// 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 = HitTestResult Function(Offset offset);
// Various states of a connected mouse device used by [MouseTracker].
class _MouseState {
_MouseState({
required PointerEvent initialEvent,
}) : _latestEvent = initialEvent;
// The list of annotations that contains this device.
//
// It uses [LinkedHashMap] to keep the insertion order.
LinkedHashMap<MouseTrackerAnnotation, Matrix4> get annotations => _annotations;
LinkedHashMap<MouseTrackerAnnotation, Matrix4> _annotations = LinkedHashMap<MouseTrackerAnnotation, Matrix4>();
LinkedHashMap<MouseTrackerAnnotation, Matrix4> replaceAnnotations(LinkedHashMap<MouseTrackerAnnotation, Matrix4> value) {
final LinkedHashMap<MouseTrackerAnnotation, Matrix4> 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.device == _latestEvent.device);
final PointerEvent previous = _latestEvent;
_latestEvent = value;
return previous;
}
int get device => latestEvent.device;
@override
String toString() {
final String describeLatestEvent = 'latestEvent: ${describeIdentity(latestEvent)}';
final String describeAnnotations = 'annotations: [list of ${annotations.length}]';
return '${describeIdentity(this)}($describeLatestEvent, $describeAnnotations)';
}
}
// The information in `MouseTracker._handleDeviceUpdate` 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 PointerEvent this.previousEvent,
}) : 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 PointerEvent this.triggeringEvent,
});
/// The annotations that the device is hovering before the update.
///
/// It is never null.
final LinkedHashMap<MouseTrackerAnnotation, Matrix4> lastAnnotations;
/// The annotations that the device is hovering after the update.
///
/// It is never null.
final LinkedHashMap<MouseTrackerAnnotation, Matrix4> 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;
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!;
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<Map<MouseTrackerAnnotation, Matrix4>>('lastAnnotations', lastAnnotations));
properties.add(DiagnosticsProperty<Map<MouseTrackerAnnotation, Matrix4>>('nextAnnotations', nextAnnotations));
}
}
/// Tracks the relationship between mouse devices and annotations, and
/// triggers mouse events and cursor changes accordingly.
///
/// The [MouseTracker] tracks the relationship between mouse devices and
/// [MouseTrackerAnnotation], notified by [updateWithEvent] and
/// [updateAllDevices]. At every update, [MouseTracker] triggers the following
/// changes if applicable:
///
/// * Dispatches mouse-related pointer events (pointer enter, hover, and exit).
/// * Changes mouse cursors.
/// * Notifies when [mouseIsConnected] changes.
///
/// 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
/// [RendererBinding].
class MouseTracker extends ChangeNotifier {
final MouseCursorManager _mouseCursorMixin = MouseCursorManager(
SystemMouseCursors.basic,
);
// Tracks the state of connected mouse devices.
//
// It is the source of truth for the list of connected mouse devices, and
// 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`.
//
// 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;
}
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;
}
LinkedHashMap<MouseTrackerAnnotation, Matrix4> _hitTestResultToAnnotations(HitTestResult result) {
final LinkedHashMap<MouseTrackerAnnotation, Matrix4> annotations = LinkedHashMap<MouseTrackerAnnotation, Matrix4>();
for (final HitTestEntry entry in result.path) {
final Object target = entry.target;
if (target is MouseTrackerAnnotation) {
annotations[target] = 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 `hitTest`.
LinkedHashMap<MouseTrackerAnnotation, Matrix4> _findAnnotations(_MouseState state, MouseDetectorAnnotationFinder hitTest) {
final Offset globalPosition = state.latestEvent.position;
final int device = state.device;
if (!_mouseStates.containsKey(device)) {
return LinkedHashMap<MouseTrackerAnnotation, Matrix4>();
}
return _hitTestResultToAnnotations(hitTest(globalPosition));
}
// A callback that is called on the update of a device.
//
// An event (not necessarily a pointer event) that might change the
// relationship between mouse devices and [MouseTrackerAnnotation]s is called
// a _device update_. This method should be called at each such update.
//
// 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.
//
// Calls of this method must be wrapped in `_deviceUpdatePhase`.
void _handleDeviceUpdate(_MouseTrackerUpdateDetails details) {
assert(_debugDuringDeviceUpdate);
_handleDeviceUpdateMouseEvents(details);
_mouseCursorMixin.handleDeviceCursorUpdate(
details.device,
details.triggeringEvent,
details.nextAnnotations.keys.map((MouseTrackerAnnotation annotation) => annotation.cursor),
);
}
/// Whether or not at least one mouse is connected and has produced events.
bool get mouseIsConnected => _mouseStates.isNotEmpty;
/// 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. It is typically called by
/// [RendererBinding], and should be called with all events received, and let
/// [MouseTracker] filter which to react to.
///
/// The `getResult` is a function to return the hit test result at the
/// position of the event. It should not return a cached hit test
/// result, because the cache would not change during a tap sequence.
void updateWithEvent(PointerEvent event, ValueGetter<HitTestResult> getResult) {
if (event.kind != PointerDeviceKind.mouse) {
return;
}
if (event is PointerSignalEvent) {
return;
}
final HitTestResult result = event is PointerRemovedEvent ? HitTestResult() : getResult();
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) {
if (event is PointerRemovedEvent) {
return;
}
_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 LinkedHashMap<MouseTrackerAnnotation, Matrix4> nextAnnotations = event is PointerRemovedEvent ?
LinkedHashMap<MouseTrackerAnnotation, Matrix4>() :
_hitTestResultToAnnotations(result);
final LinkedHashMap<MouseTrackerAnnotation, Matrix4> lastAnnotations = targetState.replaceAnnotations(nextAnnotations);
_handleDeviceUpdate(_MouseTrackerUpdateDetails.byPointerEvent(
lastAnnotations: lastAnnotations,
nextAnnotations: nextAnnotations,
previousEvent: lastEvent,
triggeringEvent: event,
));
});
});
}
/// 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 acquires 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, and check if necessary changes need to be
/// made.
void updateAllDevices(MouseDetectorAnnotationFinder hitTest) {
_deviceUpdatePhase(() {
for (final _MouseState dirtyState in _mouseStates.values) {
final PointerEvent lastEvent = dirtyState.latestEvent;
final LinkedHashMap<MouseTrackerAnnotation, Matrix4> nextAnnotations = _findAnnotations(dirtyState, hitTest);
final LinkedHashMap<MouseTrackerAnnotation, Matrix4> lastAnnotations = dirtyState.replaceAnnotations(nextAnnotations);
_handleDeviceUpdate(_MouseTrackerUpdateDetails.byNewFrame(
lastAnnotations: lastAnnotations,
nextAnnotations: nextAnnotations,
previousEvent: lastEvent,
));
}
});
}
/// Returns the active mouse cursor for a device.
///
/// The return value is the last [MouseCursor] activated onto this device, even
/// if the activation failed.
///
/// This function is only active when asserts are enabled. In release builds,
/// it always returns null.
@visibleForTesting
MouseCursor? debugDeviceActiveCursor(int device) {
return _mouseCursorMixin.debugDeviceActiveCursor(device);
}
// Handles device update and dispatches mouse event callbacks.
static void _handleDeviceUpdateMouseEvents(_MouseTrackerUpdateDetails details) {
final PointerEvent latestEvent = details.latestEvent;
final LinkedHashMap<MouseTrackerAnnotation, Matrix4> lastAnnotations = details.lastAnnotations;
final LinkedHashMap<MouseTrackerAnnotation, Matrix4> nextAnnotations = details.nextAnnotations;
// Order is important for mouse event callbacks. The
// `_hitTestResultToAnnotations` returns annotations in the visual order
// from front to back, called the "hit-test 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
// hit-test order.
final PointerExitEvent baseExitEvent = PointerExitEvent.fromMouseEvent(latestEvent);
lastAnnotations.forEach((MouseTrackerAnnotation annotation, Matrix4 transform) {
if (!nextAnnotations.containsKey(annotation)) {
if (annotation.validForMouseTracker && annotation.onExit != null) {
annotation.onExit!(baseExitEvent.transformed(lastAnnotations[annotation]));
}
}
});
// Send enter events to annotations that are not in last but in next, in
// reverse hit-test order.
final List<MouseTrackerAnnotation> enteringAnnotations = nextAnnotations.keys.where(
(MouseTrackerAnnotation annotation) => !lastAnnotations.containsKey(annotation),
).toList();
final PointerEnterEvent baseEnterEvent = PointerEnterEvent.fromMouseEvent(latestEvent);
for (final MouseTrackerAnnotation annotation in enteringAnnotations.reversed) {
if (annotation.validForMouseTracker && annotation.onEnter != null) {
annotation.onEnter!(baseEnterEvent.transformed(nextAnnotations[annotation]));
}
}
}
}