mouse_tracking.dart 21.8 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

11 12
import 'package:vector_math/vector_math_64.dart' show Matrix4;

13 14 15
import 'mouse_cursor.dart';
import 'object.dart';

16 17
/// Signature for listening to [PointerEnterEvent] events.
///
18
/// Used by [MouseTrackerAnnotation], [MouseRegion] and [RenderMouseRegion].
19 20 21 22
typedef PointerEnterEventListener = void Function(PointerEnterEvent event);

/// Signature for listening to [PointerExitEvent] events.
///
23
/// Used by [MouseTrackerAnnotation], [MouseRegion] and [RenderMouseRegion].
24 25 26 27
typedef PointerExitEventListener = void Function(PointerExitEvent event);

/// Signature for listening to [PointerHoverEvent] events.
///
28
/// Used by [MouseTrackerAnnotation], [MouseRegion] and [RenderMouseRegion].
29 30
typedef PointerHoverEventListener = void Function(PointerHoverEvent event);

31
/// The annotation object used to annotate regions that are interested in mouse
32 33
/// movements.
///
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
/// To use an annotation, push it with [AnnotatedRegionLayer] during painting.
/// The annotation's callbacks or configurations will be used depending on the
/// relationship between annotations and mouse pointers.
///
/// A [RenderObject] who uses this class must not dispose this class in its
/// `detach`, even if it recreates a new one in `attach`, because the object
/// might be detached and attached during the same frame during a reparent, and
/// replacing the `MouseTrackerAnnotation` will cause an unnecessary `onExit` and
/// `onEnter`.
///
/// This class is also the type parameter of the annotation search started by
/// [BaseMouseTracker].
///
/// See also:
///
///  * [BaseMouseTracker], which uses [MouseTrackerAnnotation].
50
class MouseTrackerAnnotation with Diagnosticable {
51
  /// Creates an immutable [MouseTrackerAnnotation].
52 53
  ///
  /// All arguments are optional. The [cursor] must not be null.
54 55 56
  const MouseTrackerAnnotation({
    this.onEnter,
    this.onExit,
57
    this.cursor = MouseCursor.defer,
58
    this.validForMouseTracker = true,
59
  }) : assert(cursor != null);
60

61
  /// Triggered when a mouse pointer, with or without buttons pressed, has
62
  /// entered the region and [validForMouseTracker] is true.
63
  ///
64 65 66 67
  /// This callback is triggered when the pointer has started to be contained by
  /// the region, either due to a pointer event, or due to the movement or
  /// disappearance of the region. This method is always matched by a later
  /// [onExit].
68 69 70 71
  ///
  /// See also:
  ///
  ///  * [onExit], which is triggered when a mouse pointer exits the region.
72
  ///  * [MouseRegion.onEnter], which uses this callback.
73
  final PointerEnterEventListener? onEnter;
74

75
  /// Triggered when a mouse pointer, with or without buttons pressed, has
76
  /// exited the region and [validForMouseTracker] is true.
77
  ///
78
  /// This callback is triggered when the pointer has stopped being contained
79 80
  /// by the region, either due to a pointer event, or due to the movement or
  /// disappearance of the region. This method always matches an earlier
81 82 83 84 85
  /// [onEnter].
  ///
  /// See also:
  ///
  ///  * [onEnter], which is triggered when a mouse pointer enters the region.
86
  ///  * [MouseRegion.onExit], which uses this callback, but is not triggered in
87
  ///    certain cases and does not always match its earlier [MouseRegion.onEnter].
88
  final PointerExitEventListener? onExit;
89

90 91 92 93 94
  /// The mouse cursor for mouse pointers that are hovering over the region.
  ///
  /// When a mouse enters the region, its cursor will be changed to the [cursor].
  /// When the mouse leaves the region, the cursor will be set by the region
  /// found at the new location.
95
  ///
96
  /// Defaults to [MouseCursor.defer], deferring the choice of cursor to the next
97
  /// region behind it in hit-test order.
98 99 100 101 102 103
  ///
  /// See also:
  ///
  ///  * [MouseRegion.cursor], which provide values to this field.
  final MouseCursor cursor;

104 105 106 107 108 109 110 111 112
  /// Whether this is included when [MouseTracker] collects the list of annotations.
  ///
  /// If [validForMouseTracker] is false, this object is excluded from the current annotation list
  /// even if it's included in the hit test, affecting mouse-related behavior such as enter events,
  /// exit events, and mouse cursors. The [validForMouseTracker] does not affect hit testing.
  ///
  /// The [validForMouseTracker] is true for [MouseTrackerAnnotation]s built by the constructor.
  final bool validForMouseTracker;

113
  @override
114 115
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
116
    properties.add(FlagsSummary<Function?>(
117
      'callbacks',
118
      <String, Function?> {
119 120 121 122 123
        'enter': onEnter,
        'exit': onExit,
      },
      ifEmpty: '<none>',
    ));
124
    properties.add(DiagnosticsProperty<MouseCursor>('cursor', cursor, defaultValue: MouseCursor.defer));
125 126 127
  }
}

128
/// Signature for searching for [MouseTrackerAnnotation]s at the given offset.
129
///
130
/// It is used by the [BaseMouseTracker] to fetch annotations for the mouse
131
/// position.
132
typedef MouseDetectorAnnotationFinder = HitTestResult Function(Offset offset);
133

134
// Various states of a connected mouse device used by [BaseMouseTracker].
135 136
class _MouseState {
  _MouseState({
137
    required PointerEvent initialEvent,
138 139
  }) : assert(initialEvent != null),
       _latestEvent = initialEvent;
140

141
  // The list of annotations that contains this device.
142
  //
143 144 145
  // It uses [LinkedHashMap] to keep the insertion order.
  LinkedHashMap<MouseTrackerAnnotation, Matrix4> get annotations => _annotations;
  LinkedHashMap<MouseTrackerAnnotation, Matrix4> _annotations = LinkedHashMap<MouseTrackerAnnotation, Matrix4>();
146

147
  LinkedHashMap<MouseTrackerAnnotation, Matrix4> replaceAnnotations(LinkedHashMap<MouseTrackerAnnotation, Matrix4> value) {
148
    assert(value != null);
149
    final LinkedHashMap<MouseTrackerAnnotation, Matrix4> previous = _annotations;
150 151 152 153 154 155 156
    _annotations = value;
    return previous;
  }

  // The most recently processed mouse event observed from this device.
  PointerEvent get latestEvent => _latestEvent;
  PointerEvent _latestEvent;
157 158

  PointerEvent replaceLatestEvent(PointerEvent value) {
159
    assert(value != null);
160 161
    assert(value.device == _latestEvent.device);
    final PointerEvent previous = _latestEvent;
162
    _latestEvent = value;
163
    return previous;
164 165
  }

166
  int get device => latestEvent.device;
167 168 169

  @override
  String toString() {
170
    final String describeLatestEvent = 'latestEvent: ${describeIdentity(latestEvent)}';
171 172
    final String describeAnnotations = 'annotations: [list of ${annotations.length}]';
    return '${describeIdentity(this)}($describeLatestEvent, $describeAnnotations)';
173 174 175
  }
}

176 177 178 179 180 181 182 183 184 185 186 187
/// Used by [BaseMouseTracker] 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({
188 189
    required this.lastAnnotations,
    required this.nextAnnotations,
190
    required PointerEvent this.previousEvent,
191 192 193 194 195 196 197 198 199 200
  }) : 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({
201 202
    required this.lastAnnotations,
    required this.nextAnnotations,
203
    this.previousEvent,
204
    required PointerEvent this.triggeringEvent,
205 206 207 208 209 210 211
  }) : assert(triggeringEvent != null),
       assert(lastAnnotations != null),
       assert(nextAnnotations != null);

  /// The annotations that the device is hovering before the update.
  ///
  /// It is never null.
212
  final LinkedHashMap<MouseTrackerAnnotation, Matrix4> lastAnnotations;
213 214 215 216

  /// The annotations that the device is hovering after the update.
  ///
  /// It is never null.
217
  final LinkedHashMap<MouseTrackerAnnotation, Matrix4> nextAnnotations;
218 219 220 221 222 223 224 225 226

  /// 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]).
227
  final PointerEvent? previousEvent;
228 229 230 231

  /// The event that triggered this update.
  ///
  /// It is non-null if and only if the update is triggered by a pointer event.
232
  final PointerEvent? triggeringEvent;
233 234 235

  /// The pointing device of this update.
  int get device {
236
    final int result = (previousEvent ?? triggeringEvent)!.device;
237 238 239 240 241 242 243 244
    assert(result != null);
    return result;
  }

  /// The last event that the device observed after the update.
  ///
  /// The [latestEvent] is never null.
  PointerEvent get latestEvent {
245
    final PointerEvent result = triggeringEvent ?? previousEvent!;
246 247 248 249 250 251 252 253 254 255
    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));
256 257
    properties.add(DiagnosticsProperty<Map<MouseTrackerAnnotation, Matrix4>>('lastAnnotations', lastAnnotations));
    properties.add(DiagnosticsProperty<Map<MouseTrackerAnnotation, Matrix4>>('nextAnnotations', nextAnnotations));
258 259 260 261 262 263
  }
}

/// A base class that tracks the relationship between mouse devices and
/// [MouseTrackerAnnotation]s.
///
264 265 266 267 268 269 270
/// An event (not necessarily a pointer event) that might change the relationship
/// between mouse devices and [MouseTrackerAnnotation]s is called a _device
/// update_.
///
/// [MouseTracker] is notified of device updates by [updateWithEvent] or
/// [updateAllDevices], and processes effects as defined in [handleDeviceUpdate]
/// by subclasses.
271
///
272 273 274
/// This class is a [ChangeNotifier] that notifies its listeners if the value of
/// [mouseIsConnected] changes.
///
275 276 277 278
/// See also:
///
///   * [MouseTracker], which is a subclass of [BaseMouseTracker] with definition
///     of how to process mouse event callbacks and mouse cursors.
279 280
///   * [MouseTrackerCursorMixin], which is a mixin for [BaseMouseTracker] that
///     defines how to process mouse cursors.
281
abstract class BaseMouseTracker extends ChangeNotifier {
282 283 284
  /// Whether or not at least one mouse is connected and has produced events.
  bool get mouseIsConnected => _mouseStates.isNotEmpty;

285 286
  // Tracks the state of connected mouse devices.
  //
287 288 289 290 291
  // It is the source of truth for the list of connected mouse devices, and is
  // consists of two parts:
  //
  //  * The mouse devices that are connected.
  //  * In which annotations each device is contained.
292 293
  final Map<int, _MouseState> _mouseStates = <int, _MouseState>{};

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
  // 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;
    }());
  }

323
  // Whether an observed event might update a device.
324
  static bool _shouldMarkStateDirty(_MouseState? state, PointerEvent event) {
325 326
    if (state == null)
      return true;
327
    assert(event != null);
328
    final PointerEvent lastEvent = state.latestEvent;
329
    assert(event.device == lastEvent.device);
330 331
    // An Added can only follow a Removed, and a Removed can only be followed
    // by an Added.
332
    assert((event is PointerAddedEvent) == (lastEvent is PointerRemovedEvent));
333 334

    // Ignore events that are unrelated to mouse tracking.
335
    if (event is PointerSignalEvent)
336 337
      return false;
    return lastEvent is PointerAddedEvent
338 339 340 341
      || event is PointerRemovedEvent
      || lastEvent.position != event.position;
  }

342 343 344 345 346 347
  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) {
348
        annotations[entry.target as MouseTrackerAnnotation] = entry.transform!;
349 350 351 352 353
      }
    }
    return annotations;
  }

354 355
  // Find the annotations that is hovered by the device of the `state`, and
  // their respective global transform matrices.
356
  //
357
  // If the device is not connected or not a mouse, an empty map is returned
358 359 360 361
  // without calling `hitTest`.
  LinkedHashMap<MouseTrackerAnnotation, Matrix4> _findAnnotations(_MouseState state, MouseDetectorAnnotationFinder hitTest) {
    assert(state != null);
    assert(hitTest != null);
362 363
    final Offset globalPosition = state.latestEvent.position;
    final int device = state.device;
364 365
    if (!_mouseStates.containsKey(device))
      return <MouseTrackerAnnotation, Matrix4>{} as LinkedHashMap<MouseTrackerAnnotation, Matrix4>;
366 367

    return _hitTestResultToAnnotations(hitTest(globalPosition));
368 369 370 371
  }

  /// A callback that is called on the update of a device.
  ///
372 373
  /// This method should be called only by [BaseMouseTracker], each time when the
  /// relationship between a device and annotations has changed.
374
  ///
375 376 377
  /// By default the [handleDeviceUpdate] does nothing effective. Subclasses
  /// should override this method to first call to their inherited
  /// [handleDeviceUpdate] method, and then process the update as desired.
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392
  ///
  /// 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.
  ///
  /// Calling of this method must be wrapped in `_deviceUpdatePhase`.
  @protected
  @mustCallSuper
  void handleDeviceUpdate(MouseTrackerUpdateDetails details) {
    assert(_debugDuringDeviceUpdate);
393 394
  }

395 396 397 398 399 400 401 402 403 404 405 406 407
  /// 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.
  ///
  /// The [updateWithEvent] will generate the new state for the pointer based on
  /// given information, and call [handleDeviceUpdate] based on the state changes.
  void updateWithEvent(PointerEvent event, ValueGetter<HitTestResult> getResult) {
    assert(event != null);
    final HitTestResult result = event is PointerRemovedEvent ? HitTestResult() : getResult();
    assert(result != null);
408 409 410
    if (event.kind != PointerDeviceKind.mouse)
      return;
    if (event is PointerSignalEvent)
411
      return;
412
    final int device = event.device;
413
    final _MouseState? existingState = _mouseStates[device];
414 415
    if (!_shouldMarkStateDirty(existingState, event))
      return;
416

417 418 419 420 421 422
    _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) {
423
          assert(event is! PointerRemovedEvent);
424 425 426 427 428 429
          _mouseStates[device] = _MouseState(initialEvent: event);
        } else {
          assert(event is! PointerAddedEvent);
          if (event is PointerRemovedEvent)
            _mouseStates.remove(event.device);
        }
430
        final _MouseState targetState = _mouseStates[device] ?? existingState!;
431 432

        final PointerEvent lastEvent = targetState.replaceLatestEvent(event);
433 434 435
        final LinkedHashMap<MouseTrackerAnnotation, Matrix4> nextAnnotations = event is PointerRemovedEvent ?
            <MouseTrackerAnnotation, Matrix4>{} as LinkedHashMap<MouseTrackerAnnotation, Matrix4> :
            _hitTestResultToAnnotations(result);
436
        final LinkedHashMap<MouseTrackerAnnotation, Matrix4> lastAnnotations = targetState.replaceAnnotations(nextAnnotations);
437 438 439 440 441 442 443 444 445

        handleDeviceUpdate(MouseTrackerUpdateDetails.byPointerEvent(
          lastAnnotations: lastAnnotations,
          nextAnnotations: nextAnnotations,
          previousEvent: lastEvent,
          triggeringEvent: event,
        ));
      });
    });
446 447
  }

448 449 450 451 452 453 454 455 456 457 458 459
  /// 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 can acquire 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, generate the new state for the pointer
  /// based on given information, and call [handleDeviceUpdate] based on the
  /// state changes.
  void updateAllDevices(MouseDetectorAnnotationFinder hitTest) {
460 461 462
    _deviceUpdatePhase(() {
      for (final _MouseState dirtyState in _mouseStates.values) {
        final PointerEvent lastEvent = dirtyState.latestEvent;
463
        final LinkedHashMap<MouseTrackerAnnotation, Matrix4> nextAnnotations = _findAnnotations(dirtyState, hitTest);
464
        final LinkedHashMap<MouseTrackerAnnotation, Matrix4> lastAnnotations = dirtyState.replaceAnnotations(nextAnnotations);
465 466 467 468 469 470

        handleDeviceUpdate(MouseTrackerUpdateDetails.byNewFrame(
          lastAnnotations: lastAnnotations,
          nextAnnotations: nextAnnotations,
          previousEvent: lastEvent,
        ));
471
      }
472
    });
473
  }
474
}
475

476 477 478 479 480 481 482 483 484 485
// A mixin for [BaseMouseTracker] that dispatches mouse events on device update.
//
// See also:
//
//  * [MouseTracker], which uses this mixin.
mixin _MouseTrackerEventMixin on BaseMouseTracker {
  // Handles device update and dispatches mouse event callbacks.
  static void _handleDeviceUpdateMouseEvents(MouseTrackerUpdateDetails details) {
    final PointerEvent latestEvent = details.latestEvent;

486 487
    final LinkedHashMap<MouseTrackerAnnotation, Matrix4> lastAnnotations = details.lastAnnotations;
    final LinkedHashMap<MouseTrackerAnnotation, Matrix4> nextAnnotations = details.nextAnnotations;
488

489 490 491 492 493 494
    // Order is important for mouse event callbacks. The `findAnnotations`
    // returns annotations in the visual order from front to back. We call
    // it the "visual order", and the opposite one "reverse visual order".
    // The algorithm here is explained in
    // https://github.com/flutter/flutter/issues/41420

495 496
    // Send exit events to annotations that are in last but not in next, in
    // visual order.
497 498 499
    final PointerExitEvent baseExitEvent = PointerExitEvent.fromMouseEvent(latestEvent);
    lastAnnotations.forEach((MouseTrackerAnnotation annotation, Matrix4 transform) {
      if (!nextAnnotations.containsKey(annotation))
500
        if (annotation.validForMouseTracker && annotation.onExit != null)
501
          annotation.onExit!(baseExitEvent.transformed(lastAnnotations[annotation]));
502
    });
503

504 505
    // Send enter events to annotations that are not in last but in next, in
    // reverse visual order.
506 507 508 509 510
    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) {
511
      if (annotation.validForMouseTracker && annotation.onEnter != null)
512
        annotation.onEnter!(baseEnterEvent.transformed(nextAnnotations[annotation]));
513 514 515
    }
  }

516 517 518 519 520
  @protected
  @override
  void handleDeviceUpdate(MouseTrackerUpdateDetails details) {
    super.handleDeviceUpdate(details);
    _handleDeviceUpdateMouseEvents(details);
521
  }
522
}
523

524
/// Tracks the relationship between mouse devices and annotations, and
525 526
/// triggers mouse events and cursor changes accordingly.
///
527
/// The [MouseTracker] tracks the relationship between mouse devices and
528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545
/// [MouseTrackerAnnotation]s, and when such relationship changes, triggers
/// the following changes if applicable:
///
///  * Dispatches mouse-related pointer events (pointer enter, hover, and exit).
///  * Notifies changes of [mouseIsConnected].
///  * Changes mouse cursors.
///
/// An instance of [MouseTracker] is owned by the global singleton of
/// [RendererBinding].
///
/// This class is a [ChangeNotifier] that notifies its listeners if the value of
/// [mouseIsConnected] changes.
///
/// See also:
///
///   * [BaseMouseTracker], which introduces more details about the timing of
///     device updates.
class MouseTracker extends BaseMouseTracker with MouseTrackerCursorMixin, _MouseTrackerEventMixin {
546
}