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