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