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'; |
6 | library; |
7 | |
8 | import 'dart:collection' show LinkedHashMap; |
9 | import 'dart:ui'; |
10 | |
11 | import 'package:flutter/foundation.dart'; |
12 | import 'package:flutter/gestures.dart'; |
13 | import 'package:flutter/services.dart'; |
14 | |
15 | import 'object.dart'; |
16 | |
17 | export '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. |
25 | typedef MouseTrackerHitTest = HitTestResult Function(Offset offset, int viewId); |
26 | |
27 | // Various states of a connected mouse device used by [MouseTracker]. |
28 | class _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 |
73 | class _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]. |
161 | class 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 | |