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 | import 'dart:collection' show LinkedHashMap; |
6 | import 'dart:ui'; |
7 | |
8 | import 'package:flutter/foundation.dart'; |
9 | import 'package:flutter/gestures.dart'; |
10 | import 'package:flutter/services.dart'; |
11 | |
12 | import 'object.dart'; |
13 | |
14 | export 'package:flutter/services.dart' show |
15 | MouseCursor, |
16 | SystemMouseCursors; |
17 | |
18 | /// Signature for hit testing at the given offset for the specified view. |
19 | /// |
20 | /// It is used by the [MouseTracker] to fetch annotations for the mouse |
21 | /// position. |
22 | typedef MouseTrackerHitTest = HitTestResult Function(Offset offset, int viewId); |
23 | |
24 | // Various states of a connected mouse device used by [MouseTracker]. |
25 | class _MouseState { |
26 | _MouseState({ |
27 | required PointerEvent initialEvent, |
28 | }) : _latestEvent = initialEvent; |
29 | |
30 | // The list of annotations that contains this device. |
31 | // |
32 | // It uses [LinkedHashMap] to keep the insertion order. |
33 | LinkedHashMap<MouseTrackerAnnotation, Matrix4> get annotations => _annotations; |
34 | LinkedHashMap<MouseTrackerAnnotation, Matrix4> _annotations = LinkedHashMap<MouseTrackerAnnotation, Matrix4>(); |
35 | |
36 | LinkedHashMap<MouseTrackerAnnotation, Matrix4> replaceAnnotations(LinkedHashMap<MouseTrackerAnnotation, Matrix4> value) { |
37 | final LinkedHashMap<MouseTrackerAnnotation, Matrix4> previous = _annotations; |
38 | _annotations = value; |
39 | return previous; |
40 | } |
41 | |
42 | // The most recently processed mouse event observed from this device. |
43 | PointerEvent get latestEvent => _latestEvent; |
44 | PointerEvent _latestEvent; |
45 | |
46 | PointerEvent replaceLatestEvent(PointerEvent value) { |
47 | assert(value.device == _latestEvent.device); |
48 | final PointerEvent previous = _latestEvent; |
49 | _latestEvent = value; |
50 | return previous; |
51 | } |
52 | |
53 | int get device => latestEvent.device; |
54 | |
55 | @override |
56 | String toString() { |
57 | final String describeLatestEvent = 'latestEvent: ${describeIdentity(latestEvent)}' ; |
58 | final String describeAnnotations = 'annotations: [list of ${annotations.length}]' ; |
59 | return ' ${describeIdentity(this)}( $describeLatestEvent, $describeAnnotations)' ; |
60 | } |
61 | } |
62 | |
63 | // The information in `MouseTracker._handleDeviceUpdate` to provide the details |
64 | // of an update of a mouse device. |
65 | // |
66 | // This class contains the information needed to handle the update that might |
67 | // change the state of a mouse device, or the [MouseTrackerAnnotation]s that |
68 | // the mouse device is hovering. |
69 | @immutable |
70 | class _MouseTrackerUpdateDetails with Diagnosticable { |
71 | /// When device update is triggered by a new frame. |
72 | /// |
73 | /// All parameters are required. |
74 | const _MouseTrackerUpdateDetails.byNewFrame({ |
75 | required this.lastAnnotations, |
76 | required this.nextAnnotations, |
77 | required PointerEvent this.previousEvent, |
78 | }) : triggeringEvent = null; |
79 | |
80 | /// When device update is triggered by a pointer event. |
81 | /// |
82 | /// The [lastAnnotations], [nextAnnotations], and [triggeringEvent] are |
83 | /// required. |
84 | const _MouseTrackerUpdateDetails.byPointerEvent({ |
85 | required this.lastAnnotations, |
86 | required this.nextAnnotations, |
87 | this.previousEvent, |
88 | required PointerEvent this.triggeringEvent, |
89 | }); |
90 | |
91 | /// The annotations that the device is hovering before the update. |
92 | /// |
93 | /// It is never null. |
94 | final LinkedHashMap<MouseTrackerAnnotation, Matrix4> lastAnnotations; |
95 | |
96 | /// The annotations that the device is hovering after the update. |
97 | /// |
98 | /// It is never null. |
99 | final LinkedHashMap<MouseTrackerAnnotation, Matrix4> nextAnnotations; |
100 | |
101 | /// The last event that the device observed before the update. |
102 | /// |
103 | /// If the update is triggered by a frame, the [previousEvent] is never null, |
104 | /// since the pointer must have been added before. |
105 | /// |
106 | /// If the update is triggered by a pointer event, the [previousEvent] is not |
107 | /// null except for cases where the event is the first event observed by the |
108 | /// pointer (which is not necessarily a [PointerAddedEvent]). |
109 | final PointerEvent? previousEvent; |
110 | |
111 | /// The event that triggered this update. |
112 | /// |
113 | /// It is non-null if and only if the update is triggered by a pointer event. |
114 | final PointerEvent? triggeringEvent; |
115 | |
116 | /// The pointing device of this update. |
117 | int get device { |
118 | final int result = (previousEvent ?? triggeringEvent)!.device; |
119 | return result; |
120 | } |
121 | |
122 | /// The last event that the device observed after the update. |
123 | /// |
124 | /// The [latestEvent] is never null. |
125 | PointerEvent get latestEvent { |
126 | final PointerEvent result = triggeringEvent ?? previousEvent!; |
127 | return result; |
128 | } |
129 | |
130 | @override |
131 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
132 | super.debugFillProperties(properties); |
133 | properties.add(IntProperty('device' , device)); |
134 | properties.add(DiagnosticsProperty<PointerEvent>('previousEvent' , previousEvent)); |
135 | properties.add(DiagnosticsProperty<PointerEvent>('triggeringEvent' , triggeringEvent)); |
136 | properties.add(DiagnosticsProperty<Map<MouseTrackerAnnotation, Matrix4>>('lastAnnotations' , lastAnnotations)); |
137 | properties.add(DiagnosticsProperty<Map<MouseTrackerAnnotation, Matrix4>>('nextAnnotations' , nextAnnotations)); |
138 | } |
139 | } |
140 | |
141 | /// Tracks the relationship between mouse devices and annotations, and |
142 | /// triggers mouse events and cursor changes accordingly. |
143 | /// |
144 | /// The [MouseTracker] tracks the relationship between mouse devices and |
145 | /// [MouseTrackerAnnotation], notified by [updateWithEvent] and |
146 | /// [updateAllDevices]. At every update, [MouseTracker] triggers the following |
147 | /// changes if applicable: |
148 | /// |
149 | /// * Dispatches mouse-related pointer events (pointer enter, hover, and exit). |
150 | /// * Changes mouse cursors. |
151 | /// * Notifies when [mouseIsConnected] changes. |
152 | /// |
153 | /// This class is a [ChangeNotifier] that notifies its listeners if the value of |
154 | /// [mouseIsConnected] changes. |
155 | /// |
156 | /// An instance of [MouseTracker] is owned by the global singleton |
157 | /// [RendererBinding]. |
158 | class MouseTracker extends ChangeNotifier { |
159 | /// Create a mouse tracker. |
160 | /// |
161 | /// The `hitTestInView` is used to find the render objects on a given |
162 | /// position in the specific view. It is typically provided by the |
163 | /// [RendererBinding]. |
164 | MouseTracker(MouseTrackerHitTest hitTestInView) |
165 | : _hitTestInView = hitTestInView; |
166 | |
167 | final MouseTrackerHitTest _hitTestInView; |
168 | |
169 | final MouseCursorManager _mouseCursorMixin = MouseCursorManager( |
170 | SystemMouseCursors.basic, |
171 | ); |
172 | |
173 | // Tracks the state of connected mouse devices. |
174 | // |
175 | // It is the source of truth for the list of connected mouse devices, and |
176 | // consists of two parts: |
177 | // |
178 | // * The mouse devices that are connected. |
179 | // * In which annotations each device is contained. |
180 | final Map<int, _MouseState> _mouseStates = <int, _MouseState>{}; |
181 | |
182 | // Used to wrap any procedure that might change `mouseIsConnected`. |
183 | // |
184 | // This method records `mouseIsConnected`, runs `task`, and calls |
185 | // [notifyListeners] at the end if the `mouseIsConnected` has changed. |
186 | void _monitorMouseConnection(VoidCallback task) { |
187 | final bool mouseWasConnected = mouseIsConnected; |
188 | task(); |
189 | if (mouseWasConnected != mouseIsConnected) { |
190 | notifyListeners(); |
191 | } |
192 | } |
193 | |
194 | bool _debugDuringDeviceUpdate = false; |
195 | // Used to wrap any procedure that might call `_handleDeviceUpdate`. |
196 | // |
197 | // In debug mode, this method uses `_debugDuringDeviceUpdate` to prevent |
198 | // `_deviceUpdatePhase` being recursively called. |
199 | void _deviceUpdatePhase(VoidCallback task) { |
200 | assert(!_debugDuringDeviceUpdate); |
201 | assert(() { |
202 | _debugDuringDeviceUpdate = true; |
203 | return true; |
204 | }()); |
205 | task(); |
206 | assert(() { |
207 | _debugDuringDeviceUpdate = false; |
208 | return true; |
209 | }()); |
210 | } |
211 | |
212 | // Whether an observed event might update a device. |
213 | static bool _shouldMarkStateDirty(_MouseState? state, PointerEvent event) { |
214 | if (state == null) { |
215 | return true; |
216 | } |
217 | final PointerEvent lastEvent = state.latestEvent; |
218 | assert(event.device == lastEvent.device); |
219 | // An Added can only follow a Removed, and a Removed can only be followed |
220 | // by an Added. |
221 | assert((event is PointerAddedEvent) == (lastEvent is PointerRemovedEvent)); |
222 | |
223 | // Ignore events that are unrelated to mouse tracking. |
224 | if (event is PointerSignalEvent) { |
225 | return false; |
226 | } |
227 | return lastEvent is PointerAddedEvent |
228 | || event is PointerRemovedEvent |
229 | || lastEvent.position != event.position; |
230 | } |
231 | |
232 | LinkedHashMap<MouseTrackerAnnotation, Matrix4> _hitTestInViewResultToAnnotations(HitTestResult result) { |
233 | final LinkedHashMap<MouseTrackerAnnotation, Matrix4> annotations = LinkedHashMap<MouseTrackerAnnotation, Matrix4>(); |
234 | for (final HitTestEntry entry in result.path) { |
235 | final Object target = entry.target; |
236 | if (target is MouseTrackerAnnotation) { |
237 | annotations[target] = entry.transform!; |
238 | } |
239 | } |
240 | return annotations; |
241 | } |
242 | |
243 | // Find the annotations that is hovered by the device of the `state`, and |
244 | // their respective global transform matrices. |
245 | // |
246 | // If the device is not connected or not a mouse, an empty map is returned |
247 | // without calling `hitTest`. |
248 | LinkedHashMap<MouseTrackerAnnotation, Matrix4> _findAnnotations(_MouseState state) { |
249 | final Offset globalPosition = state.latestEvent.position; |
250 | final int device = state.device; |
251 | final int viewId = state.latestEvent.viewId; |
252 | if (!_mouseStates.containsKey(device)) { |
253 | return LinkedHashMap<MouseTrackerAnnotation, Matrix4>(); |
254 | } |
255 | |
256 | return _hitTestInViewResultToAnnotations(_hitTestInView(globalPosition, viewId)); |
257 | } |
258 | |
259 | // A callback that is called on the update of a device. |
260 | // |
261 | // An event (not necessarily a pointer event) that might change the |
262 | // relationship between mouse devices and [MouseTrackerAnnotation]s is called |
263 | // a _device update_. This method should be called at each such update. |
264 | // |
265 | // The update can be caused by two kinds of triggers: |
266 | // |
267 | // * Triggered by the addition, movement, or removal of a pointer. Such calls |
268 | // occur during the handler of the event, indicated by |
269 | // `details.triggeringEvent` being non-null. |
270 | // * Triggered by the appearance, movement, or disappearance of an annotation. |
271 | // Such calls occur after each new frame, during the post-frame callbacks, |
272 | // indicated by `details.triggeringEvent` being null. |
273 | // |
274 | // Calls of this method must be wrapped in `_deviceUpdatePhase`. |
275 | void _handleDeviceUpdate(_MouseTrackerUpdateDetails details) { |
276 | assert(_debugDuringDeviceUpdate); |
277 | _handleDeviceUpdateMouseEvents(details); |
278 | _mouseCursorMixin.handleDeviceCursorUpdate( |
279 | details.device, |
280 | details.triggeringEvent, |
281 | details.nextAnnotations.keys.map((MouseTrackerAnnotation annotation) => annotation.cursor), |
282 | ); |
283 | } |
284 | |
285 | /// Whether or not at least one mouse is connected and has produced events. |
286 | bool get mouseIsConnected => _mouseStates.isNotEmpty; |
287 | |
288 | /// Perform a device update for one device according to the given new event. |
289 | /// |
290 | /// The [updateWithEvent] is typically called by [RendererBinding] during the |
291 | /// handler of a pointer event. All pointer events should call this method, |
292 | /// and let [MouseTracker] filter which to react to. |
293 | /// |
294 | /// The `hitTestResult` serves as an optional optimization, and is the hit |
295 | /// test result already performed by [RendererBinding] for other gestures. It |
296 | /// can be null, but when it's not null, it should be identical to the result |
297 | /// from directly calling `hitTestInView` given in the constructor (which |
298 | /// means that it should not use the cached result for [PointerMoveEvent]). |
299 | /// |
300 | /// The [updateWithEvent] is one of the two ways of updating mouse |
301 | /// states, the other one being [updateAllDevices]. |
302 | void updateWithEvent(PointerEvent event, HitTestResult? hitTestResult) { |
303 | if (event.kind != PointerDeviceKind.mouse) { |
304 | return; |
305 | } |
306 | if (event is PointerSignalEvent) { |
307 | return; |
308 | } |
309 | final HitTestResult result; |
310 | if (event is PointerRemovedEvent) { |
311 | result = HitTestResult(); |
312 | } else { |
313 | final int viewId = event.viewId; |
314 | result = hitTestResult ?? _hitTestInView(event.position, 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 | |