1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'binding.dart';
6library;
7
8import 'dart:collection' show LinkedHashMap;
9import 'dart:ui';
10
11import 'package:flutter/foundation.dart';
12import 'package:flutter/gestures.dart';
13import 'package:flutter/services.dart';
14
15import 'object.dart';
16
17export 'package:flutter/services.dart' show
18 MouseCursor,
19 SystemMouseCursors;
20
21/// Signature for hit testing at the given offset for the specified view.
22///
23/// It is used by the [MouseTracker] to fetch annotations for the mouse
24/// position.
25typedef MouseTrackerHitTest = HitTestResult Function(Offset offset, int viewId);
26
27// Various states of a connected mouse device used by [MouseTracker].
28class _MouseState {
29 _MouseState({
30 required PointerEvent initialEvent,
31 }) : _latestEvent = initialEvent;
32
33 // The list of annotations that contains this device.
34 //
35 // It uses [LinkedHashMap] to keep the insertion order.
36 LinkedHashMap<MouseTrackerAnnotation, Matrix4> get annotations => _annotations;
37 LinkedHashMap<MouseTrackerAnnotation, Matrix4> _annotations = LinkedHashMap<MouseTrackerAnnotation, Matrix4>();
38
39 LinkedHashMap<MouseTrackerAnnotation, Matrix4> replaceAnnotations(LinkedHashMap<MouseTrackerAnnotation, Matrix4> value) {
40 final LinkedHashMap<MouseTrackerAnnotation, Matrix4> previous = _annotations;
41 _annotations = value;
42 return previous;
43 }
44
45 // The most recently processed mouse event observed from this device.
46 PointerEvent get latestEvent => _latestEvent;
47 PointerEvent _latestEvent;
48
49 PointerEvent replaceLatestEvent(PointerEvent value) {
50 assert(value.device == _latestEvent.device);
51 final PointerEvent previous = _latestEvent;
52 _latestEvent = value;
53 return previous;
54 }
55
56 int get device => latestEvent.device;
57
58 @override
59 String toString() {
60 final String describeLatestEvent = 'latestEvent: ${describeIdentity(latestEvent)}';
61 final String describeAnnotations = 'annotations: [list of ${annotations.length}]';
62 return '${describeIdentity(this)}($describeLatestEvent, $describeAnnotations)';
63 }
64}
65
66// The information in `MouseTracker._handleDeviceUpdate` to provide the details
67// of an update of a mouse device.
68//
69// This class contains the information needed to handle the update that might
70// change the state of a mouse device, or the [MouseTrackerAnnotation]s that
71// the mouse device is hovering.
72@immutable
73class _MouseTrackerUpdateDetails with Diagnosticable {
74 /// When device update is triggered by a new frame.
75 ///
76 /// All parameters are required.
77 const _MouseTrackerUpdateDetails.byNewFrame({
78 required this.lastAnnotations,
79 required this.nextAnnotations,
80 required PointerEvent this.previousEvent,
81 }) : triggeringEvent = null;
82
83 /// When device update is triggered by a pointer event.
84 ///
85 /// The [lastAnnotations], [nextAnnotations], and [triggeringEvent] are
86 /// required.
87 const _MouseTrackerUpdateDetails.byPointerEvent({
88 required this.lastAnnotations,
89 required this.nextAnnotations,
90 this.previousEvent,
91 required PointerEvent this.triggeringEvent,
92 });
93
94 /// The annotations that the device is hovering before the update.
95 ///
96 /// It is never null.
97 final LinkedHashMap<MouseTrackerAnnotation, Matrix4> lastAnnotations;
98
99 /// The annotations that the device is hovering after the update.
100 ///
101 /// It is never null.
102 final LinkedHashMap<MouseTrackerAnnotation, Matrix4> nextAnnotations;
103
104 /// The last event that the device observed before the update.
105 ///
106 /// If the update is triggered by a frame, the [previousEvent] is never null,
107 /// since the pointer must have been added before.
108 ///
109 /// If the update is triggered by a pointer event, the [previousEvent] is not
110 /// null except for cases where the event is the first event observed by the
111 /// pointer (which is not necessarily a [PointerAddedEvent]).
112 final PointerEvent? previousEvent;
113
114 /// The event that triggered this update.
115 ///
116 /// It is non-null if and only if the update is triggered by a pointer event.
117 final PointerEvent? triggeringEvent;
118
119 /// The pointing device of this update.
120 int get device {
121 final int result = (previousEvent ?? triggeringEvent)!.device;
122 return result;
123 }
124
125 /// The last event that the device observed after the update.
126 ///
127 /// The [latestEvent] is never null.
128 PointerEvent get latestEvent {
129 final PointerEvent result = triggeringEvent ?? previousEvent!;
130 return result;
131 }
132
133 @override
134 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
135 super.debugFillProperties(properties);
136 properties.add(IntProperty('device', device));
137 properties.add(DiagnosticsProperty<PointerEvent>('previousEvent', previousEvent));
138 properties.add(DiagnosticsProperty<PointerEvent>('triggeringEvent', triggeringEvent));
139 properties.add(DiagnosticsProperty<Map<MouseTrackerAnnotation, Matrix4>>('lastAnnotations', lastAnnotations));
140 properties.add(DiagnosticsProperty<Map<MouseTrackerAnnotation, Matrix4>>('nextAnnotations', nextAnnotations));
141 }
142}
143
144/// Tracks the relationship between mouse devices and annotations, and
145/// triggers mouse events and cursor changes accordingly.
146///
147/// The [MouseTracker] tracks the relationship between mouse devices and
148/// [MouseTrackerAnnotation], notified by [updateWithEvent] and
149/// [updateAllDevices]. At every update, [MouseTracker] triggers the following
150/// changes if applicable:
151///
152/// * Dispatches mouse-related pointer events (pointer enter, hover, and exit).
153/// * Changes mouse cursors.
154/// * Notifies when [mouseIsConnected] changes.
155///
156/// This class is a [ChangeNotifier] that notifies its listeners if the value of
157/// [mouseIsConnected] changes.
158///
159/// An instance of [MouseTracker] is owned by the global singleton
160/// [RendererBinding].
161class MouseTracker extends ChangeNotifier {
162 /// Create a mouse tracker.
163 ///
164 /// The `hitTestInView` is used to find the render objects on a given
165 /// position in the specific view. It is typically provided by the
166 /// [RendererBinding].
167 MouseTracker(MouseTrackerHitTest hitTestInView)
168 : _hitTestInView = hitTestInView;
169
170 final MouseTrackerHitTest _hitTestInView;
171
172 final MouseCursorManager _mouseCursorMixin = MouseCursorManager(
173 SystemMouseCursors.basic,
174 );
175
176 // Tracks the state of connected mouse devices.
177 //
178 // It is the source of truth for the list of connected mouse devices, and
179 // consists of two parts:
180 //
181 // * The mouse devices that are connected.
182 // * In which annotations each device is contained.
183 final Map<int, _MouseState> _mouseStates = <int, _MouseState>{};
184
185 // Used to wrap any procedure that might change `mouseIsConnected`.
186 //
187 // This method records `mouseIsConnected`, runs `task`, and calls
188 // [notifyListeners] at the end if the `mouseIsConnected` has changed.
189 void _monitorMouseConnection(VoidCallback task) {
190 final bool mouseWasConnected = mouseIsConnected;
191 task();
192 if (mouseWasConnected != mouseIsConnected) {
193 notifyListeners();
194 }
195 }
196
197 bool _debugDuringDeviceUpdate = false;
198 // Used to wrap any procedure that might call `_handleDeviceUpdate`.
199 //
200 // In debug mode, this method uses `_debugDuringDeviceUpdate` to prevent
201 // `_deviceUpdatePhase` being recursively called.
202 void _deviceUpdatePhase(VoidCallback task) {
203 assert(!_debugDuringDeviceUpdate);
204 assert(() {
205 _debugDuringDeviceUpdate = true;
206 return true;
207 }());
208 task();
209 assert(() {
210 _debugDuringDeviceUpdate = false;
211 return true;
212 }());
213 }
214
215 // Whether an observed event might update a device.
216 static bool _shouldMarkStateDirty(_MouseState? state, PointerEvent event) {
217 if (state == null) {
218 return true;
219 }
220 final PointerEvent lastEvent = state.latestEvent;
221 assert(event.device == lastEvent.device);
222 // An Added can only follow a Removed, and a Removed can only be followed
223 // by an Added.
224 assert((event is PointerAddedEvent) == (lastEvent is PointerRemovedEvent));
225
226 // Ignore events that are unrelated to mouse tracking.
227 if (event is PointerSignalEvent) {
228 return false;
229 }
230 return lastEvent is PointerAddedEvent
231 || event is PointerRemovedEvent
232 || lastEvent.position != event.position;
233 }
234
235 LinkedHashMap<MouseTrackerAnnotation, Matrix4> _hitTestInViewResultToAnnotations(HitTestResult result) {
236 final LinkedHashMap<MouseTrackerAnnotation, Matrix4> annotations = LinkedHashMap<MouseTrackerAnnotation, Matrix4>();
237 for (final HitTestEntry entry in result.path) {
238 final Object target = entry.target;
239 if (target is MouseTrackerAnnotation) {
240 annotations[target] = entry.transform!;
241 }
242 }
243 return annotations;
244 }
245
246 // Find the annotations that is hovered by the device of the `state`, and
247 // their respective global transform matrices.
248 //
249 // If the device is not connected or not a mouse, an empty map is returned
250 // without calling `hitTest`.
251 LinkedHashMap<MouseTrackerAnnotation, Matrix4> _findAnnotations(_MouseState state) {
252 final Offset globalPosition = state.latestEvent.position;
253 final int device = state.device;
254 final int viewId = state.latestEvent.viewId;
255 if (!_mouseStates.containsKey(device)) {
256 return LinkedHashMap<MouseTrackerAnnotation, Matrix4>();
257 }
258
259 return _hitTestInViewResultToAnnotations(_hitTestInView(globalPosition, viewId));
260 }
261
262 // A callback that is called on the update of a device.
263 //
264 // An event (not necessarily a pointer event) that might change the
265 // relationship between mouse devices and [MouseTrackerAnnotation]s is called
266 // a _device update_. This method should be called at each such update.
267 //
268 // The update can be caused by two kinds of triggers:
269 //
270 // * Triggered by the addition, movement, or removal of a pointer. Such calls
271 // occur during the handler of the event, indicated by
272 // `details.triggeringEvent` being non-null.
273 // * Triggered by the appearance, movement, or disappearance of an annotation.
274 // Such calls occur after each new frame, during the post-frame callbacks,
275 // indicated by `details.triggeringEvent` being null.
276 //
277 // Calls of this method must be wrapped in `_deviceUpdatePhase`.
278 void _handleDeviceUpdate(_MouseTrackerUpdateDetails details) {
279 assert(_debugDuringDeviceUpdate);
280 _handleDeviceUpdateMouseEvents(details);
281 _mouseCursorMixin.handleDeviceCursorUpdate(
282 details.device,
283 details.triggeringEvent,
284 details.nextAnnotations.keys.map((MouseTrackerAnnotation annotation) => annotation.cursor),
285 );
286 }
287
288 /// Whether or not at least one mouse is connected and has produced events.
289 bool get mouseIsConnected => _mouseStates.isNotEmpty;
290
291 /// Perform a device update for one device according to the given new event.
292 ///
293 /// The [updateWithEvent] is typically called by [RendererBinding] during the
294 /// handler of a pointer event. All pointer events should call this method,
295 /// and let [MouseTracker] filter which to react to.
296 ///
297 /// The `hitTestResult` serves as an optional optimization, and is the hit
298 /// test result already performed by [RendererBinding] for other gestures. It
299 /// can be null, but when it's not null, it should be identical to the result
300 /// from directly calling `hitTestInView` given in the constructor (which
301 /// means that it should not use the cached result for [PointerMoveEvent]).
302 ///
303 /// The [updateWithEvent] is one of the two ways of updating mouse
304 /// states, the other one being [updateAllDevices].
305 void updateWithEvent(PointerEvent event, HitTestResult? hitTestResult) {
306 if (event.kind != PointerDeviceKind.mouse && event.kind != PointerDeviceKind.stylus) {
307 return;
308 }
309 if (event is PointerSignalEvent) {
310 return;
311 }
312 final HitTestResult result = switch (event) {
313 PointerRemovedEvent() => HitTestResult(),
314 _ => hitTestResult ?? _hitTestInView(event.position, event.viewId),
315 };
316 final int device = event.device;
317 final _MouseState? existingState = _mouseStates[device];
318 if (!_shouldMarkStateDirty(existingState, event)) {
319 return;
320 }
321
322 _monitorMouseConnection(() {
323 _deviceUpdatePhase(() {
324 // Update mouseState to the latest devices that have not been removed,
325 // so that [mouseIsConnected], which is decided by `_mouseStates`, is
326 // correct during the callbacks.
327 if (existingState == null) {
328 if (event is PointerRemovedEvent) {
329 return;
330 }
331 _mouseStates[device] = _MouseState(initialEvent: event);
332 } else {
333 assert(event is! PointerAddedEvent);
334 if (event is PointerRemovedEvent) {
335 _mouseStates.remove(event.device);
336 }
337 }
338 final _MouseState targetState = _mouseStates[device] ?? existingState!;
339
340 final PointerEvent lastEvent = targetState.replaceLatestEvent(event);
341 final LinkedHashMap<MouseTrackerAnnotation, Matrix4> nextAnnotations = event is PointerRemovedEvent ?
342 LinkedHashMap<MouseTrackerAnnotation, Matrix4>() :
343 _hitTestInViewResultToAnnotations(result);
344 final LinkedHashMap<MouseTrackerAnnotation, Matrix4> lastAnnotations = targetState.replaceAnnotations(nextAnnotations);
345
346 _handleDeviceUpdate(_MouseTrackerUpdateDetails.byPointerEvent(
347 lastAnnotations: lastAnnotations,
348 nextAnnotations: nextAnnotations,
349 previousEvent: lastEvent,
350 triggeringEvent: event,
351 ));
352 });
353 });
354 }
355
356 /// Perform a device update for all detected devices.
357 ///
358 /// The [updateAllDevices] is typically called during the post frame phase,
359 /// indicating a frame has passed and all objects have potentially moved. For
360 /// each connected device, the [updateAllDevices] will make a hit test on the
361 /// device's last seen position, and check if necessary changes need to be
362 /// made.
363 ///
364 /// The [updateAllDevices] is one of the two ways of updating mouse
365 /// states, the other one being [updateWithEvent].
366 void updateAllDevices() {
367 _deviceUpdatePhase(() {
368 for (final _MouseState dirtyState in _mouseStates.values) {
369 final PointerEvent lastEvent = dirtyState.latestEvent;
370 final LinkedHashMap<MouseTrackerAnnotation, Matrix4> nextAnnotations = _findAnnotations(dirtyState);
371 final LinkedHashMap<MouseTrackerAnnotation, Matrix4> lastAnnotations = dirtyState.replaceAnnotations(nextAnnotations);
372
373 _handleDeviceUpdate(_MouseTrackerUpdateDetails.byNewFrame(
374 lastAnnotations: lastAnnotations,
375 nextAnnotations: nextAnnotations,
376 previousEvent: lastEvent,
377 ));
378 }
379 });
380 }
381
382 /// Returns the active mouse cursor for a device.
383 ///
384 /// The return value is the last [MouseCursor] activated onto this device, even
385 /// if the activation failed.
386 ///
387 /// This function is only active when asserts are enabled. In release builds,
388 /// it always returns null.
389 @visibleForTesting
390 MouseCursor? debugDeviceActiveCursor(int device) {
391 return _mouseCursorMixin.debugDeviceActiveCursor(device);
392 }
393
394 // Handles device update and dispatches mouse event callbacks.
395 static void _handleDeviceUpdateMouseEvents(_MouseTrackerUpdateDetails details) {
396 final PointerEvent latestEvent = details.latestEvent;
397
398 final LinkedHashMap<MouseTrackerAnnotation, Matrix4> lastAnnotations = details.lastAnnotations;
399 final LinkedHashMap<MouseTrackerAnnotation, Matrix4> nextAnnotations = details.nextAnnotations;
400
401 // Order is important for mouse event callbacks. The
402 // `_hitTestInViewResultToAnnotations` returns annotations in the visual order
403 // from front to back, called the "hit-test order". The algorithm here is
404 // explained in https://github.com/flutter/flutter/issues/41420
405
406 // Send exit events to annotations that are in last but not in next, in
407 // hit-test order.
408 final PointerExitEvent baseExitEvent = PointerExitEvent.fromMouseEvent(latestEvent);
409 lastAnnotations.forEach((MouseTrackerAnnotation annotation, Matrix4 transform) {
410 if (annotation.validForMouseTracker && !nextAnnotations.containsKey(annotation)) {
411 annotation.onExit?.call(baseExitEvent.transformed(lastAnnotations[annotation]));
412 }
413 });
414
415 // Send enter events to annotations that are not in last but in next, in
416 // reverse hit-test order.
417 final List<MouseTrackerAnnotation> enteringAnnotations = nextAnnotations.keys.where(
418 (MouseTrackerAnnotation annotation) => !lastAnnotations.containsKey(annotation),
419 ).toList();
420 final PointerEnterEvent baseEnterEvent = PointerEnterEvent.fromMouseEvent(latestEvent);
421 for (final MouseTrackerAnnotation annotation in enteringAnnotations.reversed) {
422 if (annotation.validForMouseTracker) {
423 annotation.onEnter?.call(baseEnterEvent.transformed(nextAnnotations[annotation]));
424 }
425 }
426 }
427}
428