Unverified Commit ab3c5bfc authored by Tong Mu's avatar Tong Mu Committed by GitHub

Remove single view assumption from MouseTracker, and unify its hit testing code flow (#127060)

This is a refactor to make `MouseTracker` use the same callback for both kinds of device update. Instead of using two different callbacks for the two device updating methods, `MouseTracker` now receives a hit testing callback at construction, which is the same hit testing method as the one used for other gestures.

This PR not only makes the code cleaner, but also removes the single view assumption from `MouseTracker`, whose code no longer refers to `RendererBinding.renderView`. In the future, we only need to modify `hitTest` (which we will have to do to support gestures for multi-view anyway) to make mouse tracker support multi-view.
parent 0d95243f
......@@ -337,8 +337,18 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
/// State for all pointers which are currently down.
///
/// The state of hovering pointers is not tracked because that would require
/// hit-testing on every frame.
/// This map caches the hit test result done when the pointer goes down
/// ([PointerDownEvent] and [PointerPanZoomStartEvent]). This hit test result
/// will be used throughout the entire pointer interaction; that is, the
/// pointer is seen as pointing to the same place even if it has moved away
/// until pointer goes up ([PointerUpEvent] and [PointerPanZoomEndEvent]).
/// This matches the expected gesture interaction with a button, and allows
/// devices that don't support hovering to perform as few hit tests as
/// possible.
///
/// On the other hand, hovering requires hit testing on almost every frame.
/// This is handled in [RendererBinding] and [MouseTracker], and will ignore
/// the results cached here.
final Map<int, HitTestResult> _hitTests = <int, HitTestResult>{};
/// Dispatch an event to the targets found by a hit test on its position.
......
......@@ -315,18 +315,21 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
@visibleForTesting
void initMouseTracker([MouseTracker? tracker]) {
_mouseTracker?.dispose();
_mouseTracker = tracker ?? MouseTracker();
_mouseTracker = tracker ?? MouseTracker((Offset position, int viewId) {
final HitTestResult result = HitTestResult();
hitTestInView(result, position, viewId);
return result;
});
}
@override // from GestureBinding
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
_mouseTracker!.updateWithEvent(
event,
// Enter and exit events should be triggered with or without buttons
// pressed. When the button is pressed, normal hit test uses a cached
// When the button is pressed, normal hit test uses a cached
// result, but MouseTracker requires that the hit test is re-executed to
// update the hovering events.
() => (hitTestResult == null || event is PointerMoveEvent) ? renderView.hitTestMouseTrackers(event.position) : hitTestResult,
event is PointerMoveEvent ? null : hitTestResult,
);
super.dispatchEvent(event, hitTestResult);
}
......@@ -372,7 +375,7 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
_debugMouseTrackerUpdateScheduled = false;
return true;
}());
_mouseTracker!.updateAllDevices(renderView.hitTestMouseTrackers);
_mouseTracker!.updateAllDevices();
});
}
......@@ -518,8 +521,16 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
await endOfFrame;
}
late final int _implicitViewId = platformDispatcher.implicitView!.viewId;
@override
void hitTestInView(HitTestResult result, Offset position, int viewId) {
// Currently Flutter only supports one view, the implicit view `renderView`.
// TODO(dkwingsmt): After Flutter supports multi-view, look up the correct
// render view for the ID.
// https://github.com/flutter/flutter/issues/121573
assert(viewId == _implicitViewId,
'Unexpected view ID $viewId (expecting implicit view ID $_implicitViewId)');
assert(viewId == renderView.flutterView.viewId);
renderView.hitTest(result, position: position);
super.hitTestInView(result, position, viewId);
......
......@@ -20,11 +20,11 @@ export 'package:flutter/services.dart' show
MouseCursor,
SystemMouseCursors;
/// Signature for searching for [MouseTrackerAnnotation]s at the given offset.
/// Signature for hit testing at the given offset for the specified view.
///
/// It is used by the [MouseTracker] to fetch annotations for the mouse
/// position.
typedef MouseDetectorAnnotationFinder = HitTestResult Function(Offset offset);
typedef MouseTrackerHitTest = HitTestResult Function(Offset offset, int viewId);
// Various states of a connected mouse device used by [MouseTracker].
class _MouseState {
......@@ -161,6 +161,16 @@ class _MouseTrackerUpdateDetails with Diagnosticable {
/// An instance of [MouseTracker] is owned by the global singleton
/// [RendererBinding].
class MouseTracker extends ChangeNotifier {
/// 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;
final MouseCursorManager _mouseCursorMixin = MouseCursorManager(
SystemMouseCursors.basic,
);
......@@ -224,7 +234,7 @@ class MouseTracker extends ChangeNotifier {
|| lastEvent.position != event.position;
}
LinkedHashMap<MouseTrackerAnnotation, Matrix4> _hitTestResultToAnnotations(HitTestResult result) {
LinkedHashMap<MouseTrackerAnnotation, Matrix4> _hitTestInViewResultToAnnotations(HitTestResult result) {
final LinkedHashMap<MouseTrackerAnnotation, Matrix4> annotations = LinkedHashMap<MouseTrackerAnnotation, Matrix4>();
for (final HitTestEntry entry in result.path) {
final Object target = entry.target;
......@@ -240,14 +250,15 @@ class MouseTracker extends ChangeNotifier {
//
// If the device is not connected or not a mouse, an empty map is returned
// without calling `hitTest`.
LinkedHashMap<MouseTrackerAnnotation, Matrix4> _findAnnotations(_MouseState state, MouseDetectorAnnotationFinder hitTest) {
LinkedHashMap<MouseTrackerAnnotation, Matrix4> _findAnnotations(_MouseState state) {
final Offset globalPosition = state.latestEvent.position;
final int device = state.device;
final int viewId = state.latestEvent.viewId;
if (!_mouseStates.containsKey(device)) {
return LinkedHashMap<MouseTrackerAnnotation, Matrix4>();
}
return _hitTestResultToAnnotations(hitTest(globalPosition));
return _hitTestInViewResultToAnnotations(_hitTestInView(globalPosition, viewId));
}
// A callback that is called on the update of a device.
......@@ -279,25 +290,34 @@ class MouseTracker extends ChangeNotifier {
/// Whether or not at least one mouse is connected and has produced events.
bool get mouseIsConnected => _mouseStates.isNotEmpty;
/// Trigger a device update with a new event and its corresponding hit test
/// result.
/// 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.
///
/// The [updateWithEvent] indicates that an event has been observed, and is
/// called during the handler of the event. It is typically called by
/// [RendererBinding], and should be called with all events received, and let
/// [MouseTracker] filter which to react to.
/// 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]).
///
/// The `getResult` is a function to return the hit test result at the
/// position of the event. It should not return a cached hit test
/// result, because the cache would not change during a tap sequence.
void updateWithEvent(PointerEvent event, ValueGetter<HitTestResult> getResult) {
/// The [updateWithEvent] is one of the two ways of updating mouse
/// states, the other one being [updateAllDevices].
void updateWithEvent(PointerEvent event, HitTestResult? hitTestResult) {
if (event.kind != PointerDeviceKind.mouse) {
return;
}
if (event is PointerSignalEvent) {
return;
}
final HitTestResult result = event is PointerRemovedEvent ? HitTestResult() : getResult();
final HitTestResult result;
if (event is PointerRemovedEvent) {
result = HitTestResult();
} else {
final int viewId = event.viewId;
result = hitTestResult ?? _hitTestInView(event.position, viewId);
}
final int device = event.device;
final _MouseState? existingState = _mouseStates[device];
if (!_shouldMarkStateDirty(existingState, event)) {
......@@ -325,7 +345,7 @@ class MouseTracker extends ChangeNotifier {
final PointerEvent lastEvent = targetState.replaceLatestEvent(event);
final LinkedHashMap<MouseTrackerAnnotation, Matrix4> nextAnnotations = event is PointerRemovedEvent ?
LinkedHashMap<MouseTrackerAnnotation, Matrix4>() :
_hitTestResultToAnnotations(result);
_hitTestInViewResultToAnnotations(result);
final LinkedHashMap<MouseTrackerAnnotation, Matrix4> lastAnnotations = targetState.replaceAnnotations(nextAnnotations);
_handleDeviceUpdate(_MouseTrackerUpdateDetails.byPointerEvent(
......@@ -338,21 +358,21 @@ class MouseTracker extends ChangeNotifier {
});
}
/// Trigger a device update for all detected devices.
/// Perform 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 acquires 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, and check if necessary changes need to be
/// 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
/// made.
void updateAllDevices(MouseDetectorAnnotationFinder hitTest) {
///
/// The [updateAllDevices] is one of the two ways of updating mouse
/// states, the other one being [updateWithEvent].
void updateAllDevices() {
_deviceUpdatePhase(() {
for (final _MouseState dirtyState in _mouseStates.values) {
final PointerEvent lastEvent = dirtyState.latestEvent;
final LinkedHashMap<MouseTrackerAnnotation, Matrix4> nextAnnotations = _findAnnotations(dirtyState, hitTest);
final LinkedHashMap<MouseTrackerAnnotation, Matrix4> nextAnnotations = _findAnnotations(dirtyState);
final LinkedHashMap<MouseTrackerAnnotation, Matrix4> lastAnnotations = dirtyState.replaceAnnotations(nextAnnotations);
_handleDeviceUpdate(_MouseTrackerUpdateDetails.byNewFrame(
......@@ -384,7 +404,7 @@ class MouseTracker extends ChangeNotifier {
final LinkedHashMap<MouseTrackerAnnotation, Matrix4> nextAnnotations = details.nextAnnotations;
// Order is important for mouse event callbacks. The
// `_hitTestResultToAnnotations` returns annotations in the visual order
// `_hitTestInViewResultToAnnotations` returns annotations in the visual order
// from front to back, called the "hit-test order". The algorithm here is
// explained in https://github.com/flutter/flutter/issues/41420
......
......@@ -198,18 +198,6 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
return true;
}
/// Determines the set of mouse tracker annotations at the given position.
///
/// See also:
///
/// * [Layer.findAllAnnotations], which is used by this method to find all
/// [AnnotatedRegionLayer]s annotated for mouse tracking.
HitTestResult hitTestMouseTrackers(Offset position) {
final BoxHitTestResult result = BoxHitTestResult();
hitTest(result, position: position);
return result;
}
@override
bool get isRepaintBoundary => true;
......
......@@ -48,7 +48,7 @@ class TestMouseTrackerFlutterBinding extends BindingBase
final SchedulerPhase? lastPhase = _overridePhase;
_overridePhase = SchedulerPhase.persistentCallbacks;
addPostFrameCallback((_) {
mouseTracker.updateAllDevices(renderView.hitTestMouseTrackers);
mouseTracker.updateAllDevices();
});
_overridePhase = lastPhase;
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment