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