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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com