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