mouse_tracker.dart 16.6 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:collection' show LinkedHashMap;
6 7
import 'dart:ui';

8
import 'package:flutter/foundation.dart';
9
import 'package:flutter/gestures.dart';
10
import 'package:flutter/services.dart';
11

12 13
import 'object.dart';

14 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 26
class _MouseState {
  _MouseState({
27
    required PointerEvent initialEvent,
28
  }) : _latestEvent = initialEvent;
29

30
  // The list of annotations that contains this device.
31
  //
32 33 34
  // It uses [LinkedHashMap] to keep the insertion order.
  LinkedHashMap<MouseTrackerAnnotation, Matrix4> get annotations => _annotations;
  LinkedHashMap<MouseTrackerAnnotation, Matrix4> _annotations = LinkedHashMap<MouseTrackerAnnotation, Matrix4>();
35

36 37
  LinkedHashMap<MouseTrackerAnnotation, Matrix4> replaceAnnotations(LinkedHashMap<MouseTrackerAnnotation, Matrix4> value) {
    final LinkedHashMap<MouseTrackerAnnotation, Matrix4> previous = _annotations;
38 39 40 41 42 43 44
    _annotations = value;
    return previous;
  }

  // The most recently processed mouse event observed from this device.
  PointerEvent get latestEvent => _latestEvent;
  PointerEvent _latestEvent;
45 46 47 48

  PointerEvent replaceLatestEvent(PointerEvent value) {
    assert(value.device == _latestEvent.device);
    final PointerEvent previous = _latestEvent;
49
    _latestEvent = value;
50
    return previous;
51 52
  }

53
  int get device => latestEvent.device;
54 55 56

  @override
  String toString() {
57
    final String describeLatestEvent = 'latestEvent: ${describeIdentity(latestEvent)}';
58 59
    final String describeAnnotations = 'annotations: [list of ${annotations.length}]';
    return '${describeIdentity(this)}($describeLatestEvent, $describeAnnotations)';
60 61 62
  }
}

63 64 65 66 67 68
// 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.
69
@immutable
70
class _MouseTrackerUpdateDetails with Diagnosticable {
71 72 73
  /// When device update is triggered by a new frame.
  ///
  /// All parameters are required.
74
  const _MouseTrackerUpdateDetails.byNewFrame({
75 76
    required this.lastAnnotations,
    required this.nextAnnotations,
77
    required PointerEvent this.previousEvent,
78
  }) : triggeringEvent = null;
79 80 81 82 83

  /// When device update is triggered by a pointer event.
  ///
  /// The [lastAnnotations], [nextAnnotations], and [triggeringEvent] are
  /// required.
84
  const _MouseTrackerUpdateDetails.byPointerEvent({
85 86
    required this.lastAnnotations,
    required this.nextAnnotations,
87
    this.previousEvent,
88
    required PointerEvent this.triggeringEvent,
89
  });
90 91 92 93

  /// The annotations that the device is hovering before the update.
  ///
  /// It is never null.
94
  final LinkedHashMap<MouseTrackerAnnotation, Matrix4> lastAnnotations;
95 96 97 98

  /// The annotations that the device is hovering after the update.
  ///
  /// It is never null.
99
  final LinkedHashMap<MouseTrackerAnnotation, Matrix4> nextAnnotations;
100 101 102 103 104 105 106 107 108

  /// 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]).
109
  final PointerEvent? previousEvent;
110 111 112 113

  /// The event that triggered this update.
  ///
  /// It is non-null if and only if the update is triggered by a pointer event.
114
  final PointerEvent? triggeringEvent;
115 116 117

  /// The pointing device of this update.
  int get device {
118
    final int result = (previousEvent ?? triggeringEvent)!.device;
119 120 121 122 123 124 125
    return result;
  }

  /// The last event that the device observed after the update.
  ///
  /// The [latestEvent] is never null.
  PointerEvent get latestEvent {
126
    final PointerEvent result = triggeringEvent ?? previousEvent!;
127 128 129 130 131 132 133 134 135
    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));
136 137
    properties.add(DiagnosticsProperty<Map<MouseTrackerAnnotation, Matrix4>>('lastAnnotations', lastAnnotations));
    properties.add(DiagnosticsProperty<Map<MouseTrackerAnnotation, Matrix4>>('nextAnnotations', nextAnnotations));
138 139 140
  }
}

141 142
/// Tracks the relationship between mouse devices and annotations, and
/// triggers mouse events and cursor changes accordingly.
143
///
144 145 146 147
/// 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:
148
///
149 150 151
///  * Dispatches mouse-related pointer events (pointer enter, hover, and exit).
///  * Changes mouse cursors.
///  * Notifies when [mouseIsConnected] changes.
152
///
153 154 155
/// This class is a [ChangeNotifier] that notifies its listeners if the value of
/// [mouseIsConnected] changes.
///
156 157 158
/// An instance of [MouseTracker] is owned by the global singleton
/// [RendererBinding].
class MouseTracker extends ChangeNotifier {
159 160 161 162 163 164 165 166 167 168
  /// Create a mouse tracker.
  ///
  /// The `hitTestInView` is used to find the render objects on a given
  /// position in the specific view. It is typically provided by the
  /// [RendererBinding].
  MouseTracker(MouseTrackerHitTest hitTestInView)
    : _hitTestInView = hitTestInView;

  final MouseTrackerHitTest _hitTestInView;

169
  final MouseCursorManager _mouseCursorMixin = MouseCursorManager(
170
    SystemMouseCursors.basic,
171
  );
172

173 174
  // Tracks the state of connected mouse devices.
  //
175
  // It is the source of truth for the list of connected mouse devices, and
176 177 178 179
  // consists of two parts:
  //
  //  * The mouse devices that are connected.
  //  * In which annotations each device is contained.
180 181
  final Map<int, _MouseState> _mouseStates = <int, _MouseState>{};

182 183 184 185 186 187 188
  // 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();
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 198 199 200 201 202 203 204 205 206 207 208 209 210 211
  //
  // 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;
    }());
  }

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 220
    // An Added can only follow a Removed, and a Removed can only be followed
    // 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 229 230 231
      || event is PointerRemovedEvent
      || lastEvent.position != event.position;
  }

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 236 237
      final Object target = entry.target;
      if (target is MouseTrackerAnnotation) {
        annotations[target] = entry.transform!;
238 239 240 241 242
      }
    }
    return annotations;
  }

243 244
  // Find the annotations that is hovered by the device of the `state`, and
  // 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 250
    final Offset globalPosition = state.latestEvent.position;
    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 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
  // 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) {
276
    assert(_debugDuringDeviceUpdate);
277 278 279 280
    _handleDeviceUpdateMouseEvents(details);
    _mouseCursorMixin.handleDeviceCursorUpdate(
      details.device,
      details.triggeringEvent,
281
      details.nextAnnotations.keys.map((MouseTrackerAnnotation annotation) => annotation.cursor),
282
    );
283 284
  }

285 286 287
  /// Whether or not at least one mouse is connected and has produced events.
  bool get mouseIsConnected => _mouseStates.isNotEmpty;

288 289 290 291 292
  /// Perform a device update for one device according to the given new event.
  ///
  /// The [updateWithEvent] is typically called by [RendererBinding] during the
  /// handler of a pointer event. All pointer events should call this method,
  /// and let [MouseTracker] filter which to react to.
293
  ///
294 295 296 297 298
  /// The `hitTestResult` serves as an optional optimization, and is the hit
  /// test result already performed by [RendererBinding] for other gestures. It
  /// can be null, but when it's not null, it should be identical to the result
  /// from directly calling `hitTestInView` given in the constructor (which
  /// means that it should not use the cached result for [PointerMoveEvent]).
299
  ///
300 301 302
  /// The [updateWithEvent] is one of the two ways of updating mouse
  /// states, the other one being [updateAllDevices].
  void updateWithEvent(PointerEvent event, HitTestResult? hitTestResult) {
303
    if (event.kind != PointerDeviceKind.mouse) {
304
      return;
305 306
    }
    if (event is PointerSignalEvent) {
307
      return;
308
    }
309 310 311 312 313 314 315
    final HitTestResult result;
    if (event is PointerRemovedEvent) {
      result = HitTestResult();
    } else {
      final int viewId = event.viewId;
      result = hitTestResult ?? _hitTestInView(event.position, viewId);
    }
316
    final int device = event.device;
317
    final _MouseState? existingState = _mouseStates[device];
318
    if (!_shouldMarkStateDirty(existingState, event)) {
319
      return;
320
    }
321

322 323 324 325 326 327
    _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) {
328
          if (event is PointerRemovedEvent) {
329
            return;
330
          }
331 332 333
          _mouseStates[device] = _MouseState(initialEvent: event);
        } else {
          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 348 349 350 351 352 353
          lastAnnotations: lastAnnotations,
          nextAnnotations: nextAnnotations,
          previousEvent: lastEvent,
          triggeringEvent: event,
        ));
      });
    });
354 355
  }

356
  /// Perform a device update for all detected devices.
357 358
  ///
  /// The [updateAllDevices] is typically called during the post frame phase,
359 360 361
  /// indicating a frame has passed and all objects have potentially moved. 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
362
  /// made.
363 364 365 366
  ///
  /// The [updateAllDevices] is one of the two ways of updating mouse
  /// states, the other one being [updateWithEvent].
  void updateAllDevices() {
367 368 369
    _deviceUpdatePhase(() {
      for (final _MouseState dirtyState in _mouseStates.values) {
        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 375 376 377
          lastAnnotations: lastAnnotations,
          nextAnnotations: nextAnnotations,
          previousEvent: lastEvent,
        ));
378
      }
379
    });
380 381
  }

382 383 384 385 386 387 388 389 390 391 392 393
  /// 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);
  }

394
  // Handles device update and dispatches mouse event callbacks.
395
  static void _handleDeviceUpdateMouseEvents(_MouseTrackerUpdateDetails details) {
396 397
    final PointerEvent latestEvent = details.latestEvent;

398 399
    final LinkedHashMap<MouseTrackerAnnotation, Matrix4> lastAnnotations = details.lastAnnotations;
    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 404
    // from front to back, called the "hit-test order". The algorithm here is
    // 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 409
    final PointerExitEvent baseExitEvent = PointerExitEvent.fromMouseEvent(latestEvent);
    lastAnnotations.forEach((MouseTrackerAnnotation annotation, Matrix4 transform) {
410 411
      if (annotation.validForMouseTracker && !nextAnnotations.containsKey(annotation)) {
        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 418 419 420 421
    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) {
422 423
      if (annotation.validForMouseTracker) {
        annotation.onEnter?.call(baseEnterEvent.transformed(nextAnnotations[annotation]));
424
      }
425 426 427
    }
  }
}