// 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 LinkedHashSet; import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/scheduler.dart'; /// Signature for listening to [PointerEnterEvent] events. /// /// Used by [MouseTrackerAnnotation], [MouseRegion] and [RenderMouseRegion]. typedef PointerEnterEventListener = void Function(PointerEnterEvent event); /// Signature for listening to [PointerExitEvent] events. /// /// Used by [MouseTrackerAnnotation], [MouseRegion] and [RenderMouseRegion]. typedef PointerExitEventListener = void Function(PointerExitEvent event); /// Signature for listening to [PointerHoverEvent] events. /// /// Used by [MouseTrackerAnnotation], [MouseRegion] and [RenderMouseRegion]. typedef PointerHoverEventListener = void Function(PointerHoverEvent event); /// The annotation object used to annotate layers that are interested in mouse /// movements. /// /// This is added to a layer and managed by the [MouseRegion] widget. class MouseTrackerAnnotation with Diagnosticable { /// Creates an annotation that can be used to find layers interested in mouse /// movements. const MouseTrackerAnnotation({this.onEnter, this.onHover, this.onExit}); /// 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 /// by the annotationed region for any reason, which means it always matches a /// later [onExit]. /// /// See also: /// /// * [onExit], which is triggered when a mouse pointer exits the region. /// * [MouseRegion.onEnter], which uses this callback. final PointerEnterEventListener onEnter; /// 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. /// /// This callback is not triggered when: /// /// * An annotation that is containing the pointer has moved, and still /// contains the pointer. final PointerHoverEventListener onHover; /// Triggered when a mouse pointer, with or without buttons pressed, has /// exited the annotated region when the annotated region still exists. /// /// This callback is triggered when the pointer has stopped being contained /// by the region for any reason, which means it always matches an earlier /// [onEnter]. /// /// See also: /// /// * [onEnter], which is triggered when a mouse pointer enters the region. /// * [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]. final PointerExitEventListener onExit; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(FlagsSummary<Function>( 'callbacks', <String, Function> { 'enter': onEnter, 'hover': onHover, 'exit': onExit, }, ifEmpty: '<none>', )); } } /// 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 = Iterable<MouseTrackerAnnotation> Function(Offset offset); typedef _UpdatedDeviceHandler = void Function(_MouseState mouseState, LinkedHashSet<MouseTrackerAnnotation> previousAnnotations); // 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 [LinkedHashSet] to keep the insertion order. LinkedHashSet<MouseTrackerAnnotation> get annotations => _annotations; LinkedHashSet<MouseTrackerAnnotation> _annotations = LinkedHashSet<MouseTrackerAnnotation>(); 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) { assert(value != null); _latestEvent = value; } int get device => latestEvent.device; @override String toString() { String describeEvent(PointerEvent event) { return event == null ? 'null' : describeIdentity(event); } final String describeLatestEvent = 'latestEvent: ${describeEvent(latestEvent)}'; final String describeAnnotations = 'annotations: [list of ${annotations.length}]'; return '${describeIdentity(this)}($describeLatestEvent, $describeAnnotations)'; } } /// Maintains the relationship between mouse devices and /// [MouseTrackerAnnotation]s, and notifies interested callbacks of the changes /// thereof. /// /// 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 of /// [RendererBinding]. /// /// ### Details /// /// The state of [MouseTracker] consists of two parts: /// /// * 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. class MouseTracker extends ChangeNotifier { /// Creates a mouse tracker to keep track of mouse locations. /// /// 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. /// Usually it is [Layer.findAllAnnotations] of the root layer. /// /// All of the parameters must not be null. MouseTracker(this._router, this.annotationFinder) : assert(_router != null), assert(annotationFinder != null) { _router.addGlobalRoute(_handleEvent); } @override void dispose() { super.dispose(); _router.removeGlobalRoute(_handleEvent); } /// 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. final MouseDetectorAnnotationFinder annotationFinder; // The pointer router that the mouse tracker listens to, and receives new // mouse events from. final PointerRouter _router; // 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>{}; // 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; } // Handler for events coming from the PointerRouter. // // If the event marks the device dirty, update the device immediately. void _handleEvent(PointerEvent event) { 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; 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, previousEvent: previousEvent, unhandledEvent: event, ); }, ); } // Find the annotations that is hovered by the device of the `state`. // // If the device is not connected, an empty set is returned without calling // `annotationFinder`. LinkedHashSet<MouseTrackerAnnotation> _findAnnotations(_MouseState state) { final Offset globalPosition = state.latestEvent.position; final int device = state.device; return (_mouseStates.containsKey(device)) ? LinkedHashSet<MouseTrackerAnnotation>.from(annotationFinder(globalPosition)) : <MouseTrackerAnnotation>{} as LinkedHashSet<MouseTrackerAnnotation>; } static bool get _duringBuildPhase { return SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks; } // Update all devices, despite observing no new events. // // 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, previousEvent: mouseState.latestEvent, unhandledEvent: null, ); } ); } 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, // // * 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]; if (targetState == null) { targetState = _MouseState(initialEvent: targetEvent); _mouseStates[targetState.device] = targetState; } else { assert(targetEvent is! PointerAddedEvent); 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]; for (final _MouseState dirtyState in dirtyStates) { final LinkedHashSet<MouseTrackerAnnotation> nextAnnotations = _findAnnotations(dirtyState); final LinkedHashSet<MouseTrackerAnnotation> lastAnnotations = dirtyState.replaceAnnotations(nextAnnotations); handleUpdatedDevice(dirtyState, lastAnnotations); } assert(() { _duringDeviceUpdate = false; return true; }()); if (mouseWasConnected != mouseIsConnected) notifyListeners(); } // Dispatch callbacks related to a device after all necessary information // has been collected. // // 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. // However, one of `previousEvent` or `unhandledEvent` must not be null. static void _dispatchDeviceCallbacks({ @required LinkedHashSet<MouseTrackerAnnotation> lastAnnotations, @required LinkedHashSet<MouseTrackerAnnotation> nextAnnotations, @required PointerEvent previousEvent, @required PointerEvent unhandledEvent, }) { assert(lastAnnotations != null); assert(nextAnnotations != null); final PointerEvent latestEvent = unhandledEvent ?? previousEvent; assert(latestEvent != null); // 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 // 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), ); for (final MouseTrackerAnnotation annotation in exitingAnnotations) { if (annotation.onExit != null) { annotation.onExit(PointerExitEvent.fromMouseEvent(latestEvent)); } } // Send enter events to annotations that are not in last but in next, in // reverse visual order. final Iterable<MouseTrackerAnnotation> enteringAnnotations = nextAnnotations.difference(lastAnnotations).toList().reversed; for (final MouseTrackerAnnotation annotation in enteringAnnotations) { if (annotation.onEnter != null) { annotation.onEnter(PointerEnterEvent.fromMouseEvent(latestEvent)); } } // 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. if (unhandledEvent is PointerHoverEvent) { final Offset lastHoverPosition = previousEvent is PointerHoverEvent ? previousEvent.position : null; 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; for (final MouseTrackerAnnotation annotation in hoveringAnnotations) { if (annotation.onHover != null) { annotation.onHover(unhandledEvent); } } } } 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); if (!mouseIsConnected) return; 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; }