mouse_tracking.dart 18.7 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 LinkedHashSet;
6 7
import 'dart:ui';

8
import 'package:flutter/foundation.dart';
9
import 'package:flutter/gestures.dart';
10 11 12 13
import 'package:flutter/scheduler.dart';

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

/// Signature for listening to [PointerExitEvent] events.
///
19
/// Used by [MouseTrackerAnnotation], [MouseRegion] and [RenderMouseRegion].
20 21 22 23
typedef PointerExitEventListener = void Function(PointerExitEvent event);

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

/// The annotation object used to annotate layers that are interested in mouse
/// movements.
///
30
/// This is added to a layer and managed by the [MouseRegion] widget.
31
class MouseTrackerAnnotation with Diagnosticable {
32 33 34 35
  /// Creates an annotation that can be used to find layers interested in mouse
  /// movements.
  const MouseTrackerAnnotation({this.onEnter, this.onHover, this.onExit});

36 37 38 39
  /// Triggered when a mouse pointer, with or without buttons pressed, has
  /// entered the annotated region.
  ///
  /// This callback is triggered when the pointer has started to be contained
40 41
  /// by the annotationed region for any reason, which means it always matches a
  /// later [onExit].
42 43 44 45
  ///
  /// See also:
  ///
  ///  * [onExit], which is triggered when a mouse pointer exits the region.
46
  ///  * [MouseRegion.onEnter], which uses this callback.
47 48
  final PointerEnterEventListener onEnter;

49 50 51 52 53 54 55 56 57 58
  /// Triggered when a pointer has moved within the annotated region without
  /// buttons pressed.
  ///
  /// This callback is triggered when:
  ///
  ///  * An annotation that did not contain the pointer has moved to under a
  ///    pointer that has no buttons pressed.
  ///  * A pointer has moved onto, or moved within an annotation without buttons
  ///    pressed.
  ///
59
  /// This callback is not triggered when:
60 61 62
  ///
  ///  * An annotation that is containing the pointer has moved, and still
  ///    contains the pointer.
63 64
  final PointerHoverEventListener onHover;

65 66 67
  /// Triggered when a mouse pointer, with or without buttons pressed, has
  /// exited the annotated region when the annotated region still exists.
  ///
68 69
  /// This callback is triggered when the pointer has stopped being contained
  /// by the region for any reason, which means it always matches an earlier
70 71 72 73 74
  /// [onEnter].
  ///
  /// See also:
  ///
  ///  * [onEnter], which is triggered when a mouse pointer enters the region.
75 76 77
  ///  * [RenderMouseRegion.onExit], which uses this callback.
  ///  * [MouseRegion.onExit], which uses this callback, but is not triggered in
  ///    certain cases and does not always match its earier [MouseRegion.onEnter].
78 79 80
  final PointerExitEventListener onExit;

  @override
81 82 83 84 85 86 87 88 89 90 91
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(FlagsSummary<Function>(
      'callbacks',
      <String, Function> {
        'enter': onEnter,
        'hover': onHover,
        'exit': onExit,
      },
      ifEmpty: '<none>',
    ));
92 93 94
  }
}

95
/// Signature for searching for [MouseTrackerAnnotation]s at the given offset.
96 97 98
///
/// It is used by the [MouseTracker] to fetch annotations for the mouse
/// position.
99
typedef MouseDetectorAnnotationFinder = Iterable<MouseTrackerAnnotation> Function(Offset offset);
100

101 102 103
typedef _UpdatedDeviceHandler = void Function(_MouseState mouseState, LinkedHashSet<MouseTrackerAnnotation> previousAnnotations);

// Various states of a connected mouse device used by [MouseTracker].
104 105
class _MouseState {
  _MouseState({
106
    @required PointerEvent initialEvent,
107 108
  }) : assert(initialEvent != null),
       _latestEvent = initialEvent;
109

110
  // The list of annotations that contains this device.
111 112
  //
  // It uses [LinkedHashSet] to keep the insertion order.
113 114
  LinkedHashSet<MouseTrackerAnnotation> get annotations => _annotations;
  LinkedHashSet<MouseTrackerAnnotation> _annotations = LinkedHashSet<MouseTrackerAnnotation>();
115

116 117 118 119 120 121 122 123 124 125
  LinkedHashSet<MouseTrackerAnnotation> replaceAnnotations(LinkedHashSet<MouseTrackerAnnotation> value) {
    final LinkedHashSet<MouseTrackerAnnotation> previous = _annotations;
    _annotations = value;
    return previous;
  }

  // The most recently processed mouse event observed from this device.
  PointerEvent get latestEvent => _latestEvent;
  PointerEvent _latestEvent;
  set latestEvent(PointerEvent value) {
126
    assert(value != null);
127
    _latestEvent = value;
128 129
  }

130
  int get device => latestEvent.device;
131 132 133

  @override
  String toString() {
134
    String describeEvent(PointerEvent event) {
135
      return event == null ? 'null' : describeIdentity(event);
136 137 138 139
    }
    final String describeLatestEvent = 'latestEvent: ${describeEvent(latestEvent)}';
    final String describeAnnotations = 'annotations: [list of ${annotations.length}]';
    return '${describeIdentity(this)}($describeLatestEvent, $describeAnnotations)';
140 141 142 143 144 145
  }
}

/// Maintains the relationship between mouse devices and
/// [MouseTrackerAnnotation]s, and notifies interested callbacks of the changes
/// thereof.
146
///
147 148 149
/// This class is a [ChangeNotifier] that notifies its listeners if the value of
/// [mouseIsConnected] changes.
///
150 151
/// An instance of [MouseTracker] is owned by the global singleton of
/// [RendererBinding].
152 153 154
///
/// ### Details
///
155
/// The state of [MouseTracker] consists of two parts:
156 157 158 159 160 161 162 163 164 165 166 167
///
///  * The mouse devices that are connected.
///  * In which annotations each device is contained.
///
/// The states remain stable most of the time, and are only changed at the
/// following moments:
///
///  * An eligible [PointerEvent] has been observed, e.g. a device is added,
///    removed, or moved. In this case, the state related to this device will
///    be immediately updated.
///  * A frame has been painted. In this case, a callback will be scheduled for
///    the upcoming post-frame phase to update all devices.
168
class MouseTracker extends ChangeNotifier {
169 170
  /// Creates a mouse tracker to keep track of mouse locations.
  ///
171 172 173 174 175 176
  /// The first parameter is a [PointerRouter], which [MouseTracker] will
  /// subscribe to and receive events from. Usually it is the global singleton
  /// instance [GestureBinding.pointerRouter].
  ///
  /// The second parameter is a function with which the [MouseTracker] can
  /// search for [MouseTrackerAnnotation]s at a given position.
177
  /// Usually it is [Layer.findAllAnnotations] of the root layer.
178
  ///
179
  /// All of the parameters must not be null.
180 181
  MouseTracker(this._router, this.annotationFinder)
      : assert(_router != null),
182
        assert(annotationFinder != null) {
183
    _router.addGlobalRoute(_handleEvent);
184 185
  }

186 187 188 189 190 191
  @override
  void dispose() {
    super.dispose();
    _router.removeGlobalRoute(_handleEvent);
  }

192 193 194 195 196 197 198 199
  /// Find annotations at a given offset in global logical coordinate space
  /// in visual order from front to back.
  ///
  /// [MouseTracker] uses this callback to know which annotations are affected
  /// by each device.
  ///
  /// The annotations should be returned in visual order from front to
  /// back, so that the callbacks are called in an correct order.
200
  final MouseDetectorAnnotationFinder annotationFinder;
201

202 203 204
  // The pointer router that the mouse tracker listens to, and receives new
  // mouse events from.
  final PointerRouter _router;
205

206 207 208 209 210
  // Tracks the state of connected mouse devices.
  //
  // It is the source of truth for the list of connected mouse devices.
  final Map<int, _MouseState> _mouseStates = <int, _MouseState>{};

211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
  // Whether an observed event might update a device.
  static bool _shouldMarkStateDirty(_MouseState state, PointerEvent value) {
    if (state == null)
      return true;
    assert(value != null);
    final PointerEvent lastEvent = state.latestEvent;
    assert(value.device == lastEvent.device);
    // An Added can only follow a Removed, and a Removed can only be followed
    // by an Added.
    assert((value is PointerAddedEvent) == (lastEvent is PointerRemovedEvent));

    // Ignore events that are unrelated to mouse tracking.
    if (value is PointerSignalEvent)
      return false;
    return lastEvent is PointerAddedEvent
      || value is PointerRemovedEvent
      || lastEvent.position != value.position;
228 229 230
  }

  // Handler for events coming from the PointerRouter.
231 232
  //
  // If the event marks the device dirty, update the device immediately.
233
  void _handleEvent(PointerEvent event) {
234 235 236
    if (event.kind != PointerDeviceKind.mouse)
      return;
    if (event is PointerSignalEvent)
237
      return;
238
    final int device = event.device;
239 240 241
    final _MouseState existingState = _mouseStates[device];
    if (!_shouldMarkStateDirty(existingState, event))
      return;
242

243 244 245 246 247 248 249 250
    final PointerEvent previousEvent = existingState?.latestEvent;
    _updateDevices(
      targetEvent: event,
      handleUpdatedDevice: (_MouseState mouseState, LinkedHashSet<MouseTrackerAnnotation> previousAnnotations) {
        assert(mouseState.device == event.device);
        _dispatchDeviceCallbacks(
          lastAnnotations: previousAnnotations,
          nextAnnotations: mouseState.annotations,
251
          previousEvent: previousEvent,
252 253 254 255
          unhandledEvent: event,
        );
      },
    );
256 257
  }

258
  // Find the annotations that is hovered by the device of the `state`.
259
  //
260 261
  // If the device is not connected, an empty set is returned without calling
  // `annotationFinder`.
262 263 264
  LinkedHashSet<MouseTrackerAnnotation> _findAnnotations(_MouseState state) {
    final Offset globalPosition = state.latestEvent.position;
    final int device = state.device;
265
    return (_mouseStates.containsKey(device))
266
      ? LinkedHashSet<MouseTrackerAnnotation>.from(annotationFinder(globalPosition))
267
      : <MouseTrackerAnnotation>{} as LinkedHashSet<MouseTrackerAnnotation>;
268 269 270 271 272 273 274
  }

  static bool get _duringBuildPhase {
    return SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks;
  }

  // Update all devices, despite observing no new events.
275
  //
276 277 278 279 280 281 282 283
  // This is called after a new frame, since annotations can be moved after
  // every frame.
  void _updateAllDevices() {
    _updateDevices(
      handleUpdatedDevice: (_MouseState mouseState, LinkedHashSet<MouseTrackerAnnotation> previousAnnotations) {
        _dispatchDeviceCallbacks(
          lastAnnotations: previousAnnotations,
          nextAnnotations: mouseState.annotations,
284 285
          previousEvent: mouseState.latestEvent,
          unhandledEvent: null,
286 287
        );
      }
288
    );
289 290
  }

291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
  bool _duringDeviceUpdate = false;
  // Update device states with the change of a new event or a new frame, and
  // trigger `handleUpdateDevice` for each dirty device.
  //
  // This method is called either when a new event is observed (`targetEvent`
  // being non-null), or when no new event is observed but all devices are
  // marked dirty due to a new frame. It means that it will not happen that all
  // devices are marked dirty when a new event is unprocessed.
  //
  // This method is the moment where `_mouseState` is updated. Before
  // this method, `_mouseState` is in sync with the state before the event or
  // before the frame. During `handleUpdateDevice` and after this method,
  // `_mouseState` is in sync with the state after the event or after the frame.
  //
  // The dirty devices are decided as follows: if `targetEvent` is not null, the
  // dirty devices are the device that observed the event; otherwise all devices
  // are dirty.
  //
  // This method first keeps `_mouseStates` up to date. More specifically,
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
  //  * If an event is observed, update `_mouseStates` by inserting or removing
  //    the state that corresponds to the event if needed, then update the
  //    `latestEvent` property of this mouse state.
  //  * For each mouse state that will correspond to a dirty device, update the
  //    `annotations` property with the annotations the device is contained.
  //
  // Then, for each dirty device, `handleUpdatedDevice` is called with the
  // updated state and the annotations before the update.
  //
  // Last, the method checks if `mouseIsConnected` has been changed, and notify
  // listeners if needed.
  void _updateDevices({
    PointerEvent targetEvent,
    @required _UpdatedDeviceHandler handleUpdatedDevice,
  }) {
    assert(handleUpdatedDevice != null);
    assert(!_duringBuildPhase);
    assert(!_duringDeviceUpdate);
    final bool mouseWasConnected = mouseIsConnected;

    // If new event is not null, only the device that observed this event is
    // dirty. The target device's state is inserted into or removed from
    // `_mouseStates` if needed, stored as `targetState`, and its
    // `mostRecentDevice` is updated.
    _MouseState targetState;
    if (targetEvent != null) {
      targetState = _mouseStates[targetEvent.device];
338
      if (targetState == null) {
339 340 341
        targetState = _MouseState(initialEvent: targetEvent);
        _mouseStates[targetState.device] = targetState;
      } else {
342
        assert(targetEvent is! PointerAddedEvent);
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
        targetState.latestEvent = targetEvent;
        // 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 (targetEvent is PointerRemovedEvent)
          _mouseStates.remove(targetEvent.device);
      }
    }
    assert((targetState == null) == (targetEvent == null));

    assert(() {
      _duringDeviceUpdate = true;
      return true;
    }());
    // We can safely use `_mouseStates` here without worrying about the removed
    // state, because `targetEvent` should be null when `_mouseStates` is used.
    final Iterable<_MouseState> dirtyStates = targetEvent == null ? _mouseStates.values : <_MouseState>[targetState];
360
    for (final _MouseState dirtyState in dirtyStates) {
361 362 363
      final LinkedHashSet<MouseTrackerAnnotation> nextAnnotations = _findAnnotations(dirtyState);
      final LinkedHashSet<MouseTrackerAnnotation> lastAnnotations = dirtyState.replaceAnnotations(nextAnnotations);
      handleUpdatedDevice(dirtyState, lastAnnotations);
364
    }
365 366 367 368 369 370 371
    assert(() {
      _duringDeviceUpdate = false;
      return true;
    }());

    if (mouseWasConnected != mouseIsConnected)
      notifyListeners();
372
  }
373

374 375 376
  // Dispatch callbacks related to a device after all necessary information
  // has been collected.
  //
377 378 379 380
  // The `previousEvent` is the latest event before `unhandledEvent`. It might be
  // null, which means the update is triggered by a new event.
  // The `unhandledEvent` can be null, which means the update is not triggered
  // by an event.
381
  // However, one of `previousEvent` or `unhandledEvent` must not be null.
382
  static void _dispatchDeviceCallbacks({
383
    @required LinkedHashSet<MouseTrackerAnnotation> lastAnnotations,
384
    @required LinkedHashSet<MouseTrackerAnnotation> nextAnnotations,
385
    @required PointerEvent previousEvent,
386
    @required PointerEvent unhandledEvent,
387
  }) {
388 389
    assert(lastAnnotations != null);
    assert(nextAnnotations != null);
390 391
    final PointerEvent latestEvent = unhandledEvent ?? previousEvent;
    assert(latestEvent != null);
392 393 394 395 396 397
    // 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

398 399 400 401 402
    // Send exit events to annotations that are in last but not in next, in
    // visual order.
    final Iterable<MouseTrackerAnnotation> exitingAnnotations = lastAnnotations.where(
      (MouseTrackerAnnotation value) => !nextAnnotations.contains(value),
    );
403
    for (final MouseTrackerAnnotation annotation in exitingAnnotations) {
404
      if (annotation.onExit != null) {
405
        annotation.onExit(PointerExitEvent.fromMouseEvent(latestEvent));
406 407 408
      }
    }

409 410
    // Send enter events to annotations that are not in last but in next, in
    // reverse visual order.
411 412 413 414
    final Iterable<MouseTrackerAnnotation> enteringAnnotations =
      nextAnnotations.difference(lastAnnotations).toList().reversed;
    for (final MouseTrackerAnnotation annotation in enteringAnnotations) {
      if (annotation.onEnter != null) {
415
        annotation.onEnter(PointerEnterEvent.fromMouseEvent(latestEvent));
416 417 418
      }
    }

419 420 421
    // Send hover events to annotations that are in next, in reverse visual
    // order. The reverse visual order is chosen only because of the simplicity
    // by keeping the hover events aligned with enter events.
422
    if (unhandledEvent is PointerHoverEvent) {
423
      final Offset lastHoverPosition = previousEvent is PointerHoverEvent ? previousEvent.position : null;
424 425 426 427 428 429
      final bool pointerHasMoved = lastHoverPosition == null || lastHoverPosition != unhandledEvent.position;
      // If the hover event follows a non-hover event, or has moved since the
      // last hover, then trigger the hover callback on all annotations.
      // Otherwise, trigger the hover callback only on annotations that it
      // newly enters.
      final Iterable<MouseTrackerAnnotation> hoveringAnnotations = pointerHasMoved ? nextAnnotations.toList().reversed : enteringAnnotations;
430
      for (final MouseTrackerAnnotation annotation in hoveringAnnotations) {
431 432
        if (annotation.onHover != null) {
          annotation.onHover(unhandledEvent);
433 434 435 436 437
        }
      }
    }
  }

438 439 440 441 442 443 444 445 446 447 448 449 450 451 452
  bool _hasScheduledPostFrameCheck = false;
  /// Mark all devices as dirty, and schedule a callback that is executed in the
  /// upcoming post-frame phase to check their updates.
  ///
  /// Checking a device means to collect the annotations that the pointer
  /// hovers, and triggers necessary callbacks accordingly.
  ///
  /// Although the actual callback belongs to the scheduler's post-frame phase,
  /// this method must be called in persistent callback phase to ensure that
  /// the callback is scheduled after every frame, since every frame can change
  /// the position of annotations. Typically the method is called by
  /// [RendererBinding]'s drawing method.
  void schedulePostFrameCheck() {
    assert(_duringBuildPhase);
    assert(!_duringDeviceUpdate);
453 454
    if (!mouseIsConnected)
      return;
455 456 457 458 459 460 461 462 463 464 465 466
    if (!_hasScheduledPostFrameCheck) {
      _hasScheduledPostFrameCheck = true;
      SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
        assert(_hasScheduledPostFrameCheck);
        _hasScheduledPostFrameCheck = false;
        _updateAllDevices();
      });
    }
  }

  /// Whether or not a mouse is connected and has produced events.
  bool get mouseIsConnected => _mouseStates.isNotEmpty;
467
}