blob: 3b5fa1d44f2f57522c8313e84ca7b1afff252012 [file] [log] [blame]
// Copyright 2018 The Chromium 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:ui';
import 'package:flutter/foundation.dart' show visibleForTesting;
import 'package:flutter/scheduler.dart';
import 'events.dart';
import 'pointer_router.dart';
/// Signature for listening to [PointerEnterEvent] events.
///
/// Used by [MouseTrackerAnnotation], [Listener] and [RenderPointerListener].
typedef PointerEnterEventListener = void Function(PointerEnterEvent event);
/// Signature for listening to [PointerExitEvent] events.
///
/// Used by [MouseTrackerAnnotation], [Listener] and [RenderPointerListener].
typedef PointerExitEventListener = void Function(PointerExitEvent event);
/// Signature for listening to [PointerHoverEvent] events.
///
/// Used by [MouseTrackerAnnotation], [Listener] and [RenderPointerListener].
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 [Listener] widget.
class MouseTrackerAnnotation {
/// 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 pointer has entered the bounding box of the annotated
/// layer.
final PointerEnterEventListener onEnter;
/// Triggered when a pointer has moved within the bounding box of the
/// annotated layer.
final PointerHoverEventListener onHover;
/// Triggered when a pointer has exited the bounding box of the annotated
/// layer.
final PointerExitEventListener onExit;
@override
String toString() {
final String none = (onEnter == null && onExit == null && onHover == null) ? ' <none>' : '';
return '[$runtimeType${hashCode.toRadixString(16)}$none'
'${onEnter == null ? '' : ' onEnter'}'
'${onHover == null ? '' : ' onHover'}'
'${onExit == null ? '' : ' onExit'}]';
}
}
// Used internally by the MouseTracker for accounting for which annotation is
// active on which devices inside of the MouseTracker.
class _TrackedAnnotation {
_TrackedAnnotation(this.annotation);
final MouseTrackerAnnotation annotation;
/// Tracks devices that are currently active for this annotation.
///
/// If the mouse pointer corresponding to the integer device ID is
/// present in the Set, then it is currently inside of the annotated layer.
///
/// This is used to detect layers that used to have the mouse pointer inside
/// them, but now no longer do (to facilitate exit notification).
Set<int> activeDevices = <int>{};
}
/// Describes a function that finds an annotation given an offset in logical
/// coordinates.
///
/// It is used by the [MouseTracker] to fetch annotations for the mouse
/// position.
typedef MouseDetectorAnnotationFinder = MouseTrackerAnnotation Function(Offset offset);
/// Keeps state about which objects are interested in tracking mouse positions
/// and notifies them when a mouse pointer enters, moves, or leaves an annotated
/// region that they are interested in.
///
/// Owned by the [RendererBinding] class.
class MouseTracker {
/// Creates a mouse tracker to keep track of mouse locations.
///
/// All of the parameters must not be null.
MouseTracker(PointerRouter router, this.annotationFinder)
: assert(router != null),
assert(annotationFinder != null) {
router.addGlobalRoute(_handleEvent);
}
/// Used to find annotations at a given logical coordinate.
final MouseDetectorAnnotationFinder annotationFinder;
// The collection of annotations that are currently being tracked. They may or
// may not be active, depending on the value of _TrackedAnnotation.active.
final Map<MouseTrackerAnnotation, _TrackedAnnotation> _trackedAnnotations = <MouseTrackerAnnotation, _TrackedAnnotation>{};
/// Track an annotation so that if the mouse enters it, we send it events.
///
/// This is typically called when the [AnnotatedRegion] containing this
/// annotation has been added to the layer tree.
void attachAnnotation(MouseTrackerAnnotation annotation) {
_trackedAnnotations[annotation] = _TrackedAnnotation(annotation);
// Schedule a check so that we test this new annotation to see if the mouse
// is currently inside its region.
_scheduleMousePositionCheck();
}
/// Stops tracking an annotation, indicating that it has been removed from the
/// layer tree.
///
/// If the associated layer is not removed, and receives a hit, then
/// [collectMousePositions] will assert the next time it is called.
void detachAnnotation(MouseTrackerAnnotation annotation) {
final _TrackedAnnotation trackedAnnotation = _findAnnotation(annotation);
for (int deviceId in trackedAnnotation.activeDevices) {
annotation.onExit(PointerExitEvent.fromMouseEvent(_lastMouseEvent[deviceId]));
}
_trackedAnnotations.remove(annotation);
}
void _scheduleMousePositionCheck() {
SchedulerBinding.instance.addPostFrameCallback((Duration _) => collectMousePositions());
SchedulerBinding.instance.scheduleFrame();
}
// Handler for events coming from the PointerRouter.
void _handleEvent(PointerEvent event) {
if (event.kind != PointerDeviceKind.mouse) {
return;
}
final int deviceId = event.device;
if (_trackedAnnotations.isEmpty) {
// If we're not tracking anything, then there is no point in registering a
// frame callback or scheduling a frame. By definition there are no active
// annotations that need exiting, either.
_lastMouseEvent.remove(deviceId);
return;
}
if (event is PointerRemovedEvent) {
_lastMouseEvent.remove(deviceId);
// If the mouse was removed, then we need to schedule one more check to
// exit any annotations that were active.
_scheduleMousePositionCheck();
} else {
if (event is PointerMoveEvent || event is PointerHoverEvent || event is PointerDownEvent) {
if (!_lastMouseEvent.containsKey(deviceId) || _lastMouseEvent[deviceId].position != event.position) {
// Only schedule a frame if we have our first event, or if the
// location of the mouse has changed.
_scheduleMousePositionCheck();
}
_lastMouseEvent[deviceId] = event;
}
}
}
_TrackedAnnotation _findAnnotation(MouseTrackerAnnotation annotation) {
final _TrackedAnnotation trackedAnnotation = _trackedAnnotations[annotation];
assert(
trackedAnnotation != null,
'Unable to find annotation $annotation in tracked annotations. '
'Check that attachAnnotation has been called for all annotated layers.');
return trackedAnnotation;
}
/// Checks if the given [MouseTrackerAnnotation] is attached to this
/// [MouseTracker].
///
/// This function is only public to allow for proper testing of the
/// MouseTracker. Do not call in other contexts.
@visibleForTesting
bool isAnnotationAttached(MouseTrackerAnnotation annotation) {
return _trackedAnnotations.containsKey(annotation);
}
/// Tells interested objects that a mouse has entered, exited, or moved, given
/// a callback to fetch the [MouseTrackerAnnotation] associated with a global
/// offset.
///
/// This is called from a post-frame callback when the layer tree has been
/// updated, right after rendering the frame.
///
/// This function is only public to allow for proper testing of the
/// MouseTracker. Do not call in other contexts.
@visibleForTesting
void collectMousePositions() {
void exitAnnotation(_TrackedAnnotation trackedAnnotation, int deviceId) {
if (trackedAnnotation.annotation?.onExit != null && trackedAnnotation.activeDevices.contains(deviceId)) {
trackedAnnotation.annotation.onExit(PointerExitEvent.fromMouseEvent(_lastMouseEvent[deviceId]));
trackedAnnotation.activeDevices.remove(deviceId);
}
}
void exitAllDevices(_TrackedAnnotation trackedAnnotation) {
if (trackedAnnotation.activeDevices.isNotEmpty) {
final Set<int> deviceIds = trackedAnnotation.activeDevices.toSet();
for (int deviceId in deviceIds) {
exitAnnotation(trackedAnnotation, deviceId);
}
}
}
// This indicates that all mouse pointers were removed, or none have been
// connected yet. If no mouse is connected, then we want to make sure that
// all active annotations are exited.
if (!mouseIsConnected) {
_trackedAnnotations.values.forEach(exitAllDevices);
return;
}
for (int deviceId in _lastMouseEvent.keys) {
final PointerEvent lastEvent = _lastMouseEvent[deviceId];
final MouseTrackerAnnotation hit = annotationFinder(lastEvent.position);
// No annotation was found at this position for this deviceId, so send an
// exit to all active tracked annotations, since none of them were hit.
if (hit == null) {
// Send an exit to all tracked animations tracking this deviceId.
for (_TrackedAnnotation trackedAnnotation in _trackedAnnotations.values) {
exitAnnotation(trackedAnnotation, deviceId);
}
return;
}
final _TrackedAnnotation hitAnnotation = _findAnnotation(hit);
if (!hitAnnotation.activeDevices.contains(deviceId)) {
// A tracked annotation that just became active and needs to have an enter
// event sent to it.
hitAnnotation.activeDevices.add(deviceId);
if (hitAnnotation.annotation?.onEnter != null) {
hitAnnotation.annotation.onEnter(PointerEnterEvent.fromMouseEvent(lastEvent));
}
}
if (hitAnnotation.annotation?.onHover != null && lastEvent is PointerHoverEvent) {
hitAnnotation.annotation.onHover(lastEvent);
}
// Tell any tracked annotations that weren't hit that they are no longer
// active.
for (_TrackedAnnotation trackedAnnotation in _trackedAnnotations.values) {
if (hitAnnotation == trackedAnnotation) {
continue;
}
if (trackedAnnotation.activeDevices.contains(deviceId)) {
if (trackedAnnotation.annotation?.onExit != null) {
trackedAnnotation.annotation.onExit(PointerExitEvent.fromMouseEvent(lastEvent));
}
trackedAnnotation.activeDevices.remove(deviceId);
}
}
}
}
/// The most recent mouse event observed for each mouse device ID observed.
///
/// May be null if no mouse is connected, or hasn't produced an event yet.
/// Will not be updated unless there is at least one tracked annotation.
final Map<int, PointerEvent> _lastMouseEvent = <int, PointerEvent>{};
/// Whether or not a mouse is connected and has produced events.
bool get mouseIsConnected => _lastMouseEvent.isNotEmpty;
}