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