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

Change MouseTracker's interface for clarity. Simplify MouseRegion's implementation. (#64119)

* Redesigns the interface between MouseTracker and RendererBinding&RenderView.
* Simplifies the structure of RenderMouseRegion.
* Extracts the common utility code between mouse_tracker_test and mouse_tracker_cursor_test.
parent 1fc3a5e4
......@@ -173,8 +173,9 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
@override // from HitTestDispatcher
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
assert(!locked);
// No hit test information implies that this is a hover or pointer
// add/remove event.
// No hit test information implies that this is a pointer hover or
// add/remove event. These events are specially routed here; other events
// will be routed through the `handleEvent` below.
if (hitTestResult == null) {
assert(event is PointerHoverEvent || event is PointerAddedEvent || event is PointerRemovedEvent);
try {
......
......@@ -869,7 +869,7 @@ class PointerHoverEvent extends PointerEvent {
/// * [PointerExitEvent], which reports when the pointer has left an object.
/// * [PointerMoveEvent], which reports movement while the pointer is in
/// contact with the device.
/// * [Listener.onPointerEnter], which allows callers to be notified of these
/// * [MouseRegion.onEnter], which allows callers to be notified of these
/// events in a widget tree.
class PointerEnterEvent extends PointerEvent {
/// Creates a pointer enter event.
......@@ -1020,7 +1020,7 @@ class PointerEnterEvent extends PointerEvent {
/// * [PointerEnterEvent], which reports when the pointer has entered an object.
/// * [PointerMoveEvent], which reports movement while the pointer is in
/// contact with the device.
/// * [Listener.onPointerExit], which allows callers to be notified of these
/// * [MouseRegion.onExit], which allows callers to be notified of these
/// events in a widget tree.
class PointerExitEvent extends PointerEvent {
/// Creates a pointer exit event.
......
......@@ -248,7 +248,19 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
@visibleForTesting
void initMouseTracker([MouseTracker tracker]) {
_mouseTracker?.dispose();
_mouseTracker = tracker ?? MouseTracker(pointerRouter, renderView.hitTestMouseTrackers);
_mouseTracker = tracker ?? MouseTracker();
}
@override // from GestureBinding
void dispatchEvent(PointerEvent event, HitTestResult hitTestResult) {
if (hitTestResult != null ||
event is PointerHoverEvent ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
_mouseTracker.updateWithEvent(event,
() => hitTestResult ?? renderView.hitTestMouseTrackers(event.position));
}
super.dispatchEvent(event, hitTestResult);
}
void _handleSemanticsEnabledChanged() {
......@@ -284,7 +296,24 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
void _handlePersistentFrameCallback(Duration timeStamp) {
drawFrame();
_mouseTracker.schedulePostFrameCheck();
_scheduleMouseTrackerUpdate();
}
bool _debugMouseTrackerUpdateScheduled = false;
void _scheduleMouseTrackerUpdate() {
assert(!_debugMouseTrackerUpdateScheduled);
assert(() {
_debugMouseTrackerUpdateScheduled = true;
return true;
}());
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
assert(_debugMouseTrackerUpdateScheduled);
assert(() {
_debugMouseTrackerUpdateScheduled = false;
return true;
}());
_mouseTracker.updateAllDevices(renderView.hitTestMouseTrackers);
});
}
int _firstFrameDeferredCount = 0;
......
......@@ -12,7 +12,6 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter/services.dart';
import 'binding.dart';
import 'box.dart';
import 'layer.dart';
import 'mouse_cursor.dart';
......@@ -668,7 +667,7 @@ mixin _PlatformViewGestureMixin on RenderBox implements MouseTrackerAnnotation {
if (value != _hitTestBehavior) {
_hitTestBehavior = value;
if (owner != null)
RendererBinding.instance.mouseTracker.schedulePostFrameCheck();
markNeedsPaint();
}
}
PlatformViewHitTestBehavior _hitTestBehavior;
......
......@@ -2762,20 +2762,16 @@ class RenderMouseRegion extends RenderProxyBox implements MouseTrackerAnnotation
/// mouse region with no callbacks and cursor being [MouseCursor.defer]. The
/// [cursor] must not be null.
RenderMouseRegion({
PointerEnterEventListener onEnter,
PointerHoverEventListener onHover,
PointerExitEventListener onExit,
this.onEnter,
this.onHover,
this.onExit,
MouseCursor cursor = MouseCursor.defer,
bool opaque = true,
RenderBox child,
}) : assert(opaque != null),
assert(cursor != null),
_onEnter = onEnter,
_onHover = onHover,
_onExit = onExit,
_cursor = cursor,
_opaque = opaque,
_annotationIsActive = false,
super(child);
@protected
......@@ -2787,6 +2783,13 @@ class RenderMouseRegion extends RenderProxyBox implements MouseTrackerAnnotation
return super.hitTest(result, position: position) && _opaque;
}
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
assert(debugHandleEvent(event, entry));
if (onHover != null && event is PointerHoverEvent)
return onHover(event);
}
/// Whether this object should prevent [RenderMouseRegion]s visually behind it
/// from detecting the pointer, thus affecting how their [onHover], [onEnter],
/// and [onExit] behave.
......@@ -2806,41 +2809,19 @@ class RenderMouseRegion extends RenderProxyBox implements MouseTrackerAnnotation
set opaque(bool value) {
if (_opaque != value) {
_opaque = value;
// A repaint is needed in order to propagate the new value to
// AnnotatedRegionLayer via [paint].
_markPropertyUpdated(mustRepaint: true);
// Trigger [MouseTracker]'s device update to recalculate mouse states.
markNeedsPaint();
}
}
@override
PointerEnterEventListener get onEnter => _onEnter;
PointerEnterEventListener _onEnter;
set onEnter(PointerEnterEventListener value) {
if (_onEnter != value) {
_onEnter = value;
_markPropertyUpdated(mustRepaint: false);
}
}
PointerEnterEventListener onEnter;
@override
PointerHoverEventListener get onHover => _onHover;
PointerHoverEventListener _onHover;
set onHover(PointerHoverEventListener value) {
if (_onHover != value) {
_onHover = value;
_markPropertyUpdated(mustRepaint: false);
}
}
PointerHoverEventListener onHover;
@override
PointerExitEventListener get onExit => _onExit;
PointerExitEventListener _onExit;
set onExit(PointerExitEventListener value) {
if (_onExit != value) {
_onExit = value;
_markPropertyUpdated(mustRepaint: false);
}
}
PointerExitEventListener onExit;
@override
MouseCursor get cursor => _cursor;
......@@ -2850,61 +2831,10 @@ class RenderMouseRegion extends RenderProxyBox implements MouseTrackerAnnotation
_cursor = value;
// A repaint is needed in order to trigger a device update of
// [MouseTracker] so that this new value can be found.
_markPropertyUpdated(mustRepaint: true);
}
}
// Call this method when a property has changed and might affect the
// `_annotationIsActive` bit.
//
// If `mustRepaint` is false, this method does NOT call `markNeedsPaint`
// unless the `_annotationIsActive` bit is changed. If there is a property
// that needs updating while `_annotationIsActive` stays true, make
// `mustRepaint` true.
//
// This method must not be called during `paint`.
void _markPropertyUpdated({@required bool mustRepaint}) {
assert(owner == null || !owner.debugDoingPaint);
final bool newAnnotationIsActive = (
_onEnter != null ||
_onHover != null ||
_onExit != null ||
_cursor != MouseCursor.defer ||
opaque
) && RendererBinding.instance.mouseTracker.mouseIsConnected;
_setAnnotationIsActive(newAnnotationIsActive);
if (mustRepaint)
markNeedsPaint();
}
bool _annotationIsActive = false;
void _setAnnotationIsActive(bool value) {
final bool annotationWasActive = _annotationIsActive;
_annotationIsActive = value;
if (annotationWasActive != value) {
markNeedsPaint();
markNeedsCompositingBitsUpdate();
}
}
void _handleUpdatedMouseIsConnected() {
_markPropertyUpdated(mustRepaint: false);
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
// Add a listener to listen for changes in mouseIsConnected.
RendererBinding.instance.mouseTracker.addListener(_handleUpdatedMouseIsConnected);
_markPropertyUpdated(mustRepaint: false);
}
@override
void detach() {
RendererBinding.instance.mouseTracker.removeListener(_handleUpdatedMouseIsConnected);
super.detach();
}
@override
void performResize() {
size = constraints.biggest;
......
......@@ -4,7 +4,6 @@
// @dart = 2.8
import 'dart:collection' show LinkedHashMap;
import 'dart:developer';
import 'dart:io' show Platform;
import 'dart:ui' as ui show Scene, SceneBuilder, Window;
......@@ -18,7 +17,6 @@ import 'binding.dart';
import 'box.dart';
import 'debug.dart';
import 'layer.dart';
import 'mouse_tracking.dart';
import 'object.dart';
/// The layout constraints for the root render object.
......@@ -199,22 +197,13 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
///
/// * [Layer.findAllAnnotations], which is used by this method to find all
/// [AnnotatedRegionLayer]s annotated for mouse tracking.
LinkedHashMap<MouseTrackerAnnotation, Matrix4> hitTestMouseTrackers(Offset position) {
HitTestResult hitTestMouseTrackers(Offset position) {
// Layer hit testing is done using device pixels, so we have to convert
// the logical coordinates of the event location back to device pixels
// here.
final BoxHitTestResult result = BoxHitTestResult();
if (child != null)
child.hitTest(result, position: position);
result.add(HitTestEntry(this));
final LinkedHashMap<MouseTrackerAnnotation, Matrix4> annotations = <MouseTrackerAnnotation, Matrix4>{}
as LinkedHashMap<MouseTrackerAnnotation, Matrix4>;
for (final HitTestEntry entry in result.path) {
if (entry.target is MouseTrackerAnnotation) {
annotations[entry.target as MouseTrackerAnnotation] = entry.transform;
}
}
return annotations;
hitTest(result, position: position);
return result;
}
@override
......
// 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.
// @dart = 2.8
import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:vector_math/vector_math_64.dart' show Matrix4;
class _TestHitTester extends RenderBox {
_TestHitTester(this.hitTestOverride);
final BoxHitTest hitTestOverride;
@override
bool hitTest(BoxHitTestResult result, {ui.Offset position}) {
return hitTestOverride(result, position);
}
}
// A binding used to test MouseTracker, allowing the test to override hit test
// searching.
class TestMouseTrackerFlutterBinding extends BindingBase
with SchedulerBinding, ServicesBinding, GestureBinding, SemanticsBinding, RendererBinding {
@override
void initInstances() {
super.initInstances();
postFrameCallbacks = <void Function(Duration)>[];
}
void setHitTest(BoxHitTest hitTest) {
renderView.child = _TestHitTester(hitTest);
}
SchedulerPhase _overridePhase;
@override
SchedulerPhase get schedulerPhase => _overridePhase ?? super.schedulerPhase;
// Manually schedule a post-frame check.
//
// In real apps this is done by the renderer binding, but in tests we have to
// bypass the phase assertion of [MouseTracker.schedulePostFrameCheck].
void scheduleMouseTrackerPostFrameCheck() {
final SchedulerPhase lastPhase = _overridePhase;
_overridePhase = SchedulerPhase.persistentCallbacks;
addPostFrameCallback((_) {
mouseTracker.updateAllDevices(renderView.hitTestMouseTrackers);
});
_overridePhase = lastPhase;
}
List<void Function(Duration)> postFrameCallbacks;
// Proxy post-frame callbacks.
@override
void addPostFrameCallback(void Function(Duration) callback) {
postFrameCallbacks.add(callback);
}
void flushPostFrameCallbacks(Duration duration) {
for (final void Function(Duration) callback in postFrameCallbacks) {
callback(duration);
}
postFrameCallbacks.clear();
}
}
// An object that mocks the behavior of a render object with [MouseTrackerAnnotation].
class TestAnnotationTarget with Diagnosticable implements MouseTrackerAnnotation, HitTestTarget {
const TestAnnotationTarget({this.onEnter, this.onHover, this.onExit, this.cursor = MouseCursor.defer});
@override
final PointerEnterEventListener onEnter;
@override
final PointerHoverEventListener onHover;
@override
final PointerExitEventListener onExit;
@override
final MouseCursor cursor;
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
if (event is PointerHoverEvent)
if (onHover != null)
onHover(event);
}
}
// A hit test entry that can be assigned with a [TestAnnotationTarget] and an
// optional transform matrix.
class TestAnnotationEntry extends HitTestEntry {
TestAnnotationEntry(TestAnnotationTarget target, [Matrix4 transform])
: transform = transform ?? Matrix4.identity(), super(target);
@override
final Matrix4 transform;
}
......@@ -4,12 +4,13 @@
// @dart = 2.8
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import '../gestures/gesture_tester.dart';
import '../services/fake_platform_views.dart';
import 'rendering_tester.dart';
......@@ -73,16 +74,33 @@ void main() {
semanticsHandle.dispose();
});
testGesture('hover events are dispatched via PlatformViewController.dispatchPointerEvent', (GestureTester tester) {
test('mouse hover events are dispatched via PlatformViewController.dispatchPointerEvent', () {
layout(platformViewRenderBox);
pumpFrame(phase: EnginePhase.flushSemantics);
final TestPointer pointer = TestPointer(1, PointerDeviceKind.mouse);
tester.route(pointer.addPointer());
tester.route(pointer.hover(const Offset(10, 10)));
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(ui.PointerChange.add, const Offset(0, 0)),
_pointerData(ui.PointerChange.hover, const Offset(10, 10)),
_pointerData(ui.PointerChange.remove, const Offset(10, 10)),
]));
expect(fakePlatformViewController.dispatchedPointerEvents, isNotEmpty);
});
}, skip: isBrowser); // TODO(yjbanov): fails on Web with obscured stack trace: https://github.com/flutter/flutter/issues/42770
}
ui.PointerData _pointerData(
ui.PointerChange change,
Offset logicalPosition, {
int device = 0,
PointerDeviceKind kind = PointerDeviceKind.mouse,
}) {
return ui.PointerData(
change: change,
physicalX: logicalPosition.dx * ui.window.devicePixelRatio,
physicalY: logicalPosition.dy * ui.window.devicePixelRatio,
kind: kind,
device: device,
);
}
......@@ -4,7 +4,6 @@
// @dart = 2.8
import 'dart:collection' show LinkedHashMap;
import 'dart:typed_data';
import 'dart:ui' as ui show Gradient, Image, ImageFilter;
......@@ -491,10 +490,6 @@ void main() {
});
test('RenderMouseRegion can change properties when detached', () {
renderer.initMouseTracker(MouseTracker(
renderer.pointerRouter,
(_) => <MouseTrackerAnnotation, Matrix4>{} as LinkedHashMap<MouseTrackerAnnotation, Matrix4>,
));
final RenderMouseRegion object = RenderMouseRegion();
object
..opaque = false
......
......@@ -601,6 +601,8 @@ void main() {
// Start outside, move inside, then move outside
await gesture.moveTo(const Offset(150.0, 150.0));
await tester.pump();
expect(logs, isEmpty);
logs.clear();
await gesture.moveTo(const Offset(50.0, 50.0));
await tester.pump();
await gesture.moveTo(const Offset(150.0, 150.0));
......@@ -1106,14 +1108,15 @@ void main() {
// Same as MouseRegion, but when opaque is null, use the default value.
Widget mouseRegionWithOptionalOpaque({
void Function(PointerEnterEvent e) onEnter,
void Function(PointerHoverEvent e) onHover,
void Function(PointerExitEvent e) onExit,
Widget child,
bool opaque,
}) {
if (opaque == null) {
return MouseRegion(onEnter: onEnter, onExit: onExit, child: child);
return MouseRegion(onEnter: onEnter, onHover: onHover, onExit: onExit, child: child);
}
return MouseRegion(onEnter: onEnter, onExit: onExit, child: child, opaque: opaque);
return MouseRegion(onEnter: onEnter, onHover: onHover, onExit: onExit, child: child, opaque: opaque);
}
return Directionality(
......@@ -1122,6 +1125,7 @@ void main() {
alignment: Alignment.topLeft,
child: MouseRegion(
onEnter: (PointerEnterEvent e) { addLog('enterA'); },
onHover: (PointerHoverEvent e) { addLog('hoverA'); },
onExit: (PointerExitEvent e) { addLog('exitA'); },
child: SizedBox(
width: 150,
......@@ -1135,6 +1139,7 @@ void main() {
height: 80,
child: MouseRegion(
onEnter: (PointerEnterEvent e) { addLog('enterB'); },
onHover: (PointerHoverEvent e) { addLog('hoverB'); },
onExit: (PointerExitEvent e) { addLog('exitB'); },
),
),
......@@ -1146,6 +1151,7 @@ void main() {
child: mouseRegionWithOptionalOpaque(
opaque: opaqueC,
onEnter: (PointerEnterEvent e) { addLog('enterC'); },
onHover: (PointerHoverEvent e) { addLog('hoverC'); },
onExit: (PointerExitEvent e) { addLog('exitC'); },
),
),
......@@ -1172,31 +1178,31 @@ void main() {
// Move to the overlapping area.
await gesture.moveTo(const Offset(75, 75));
await tester.pumpAndSettle();
expect(logs, <String>['enterA', 'enterB', 'enterC']);
expect(logs, <String>['enterA', 'enterB', 'enterC', 'hoverA', 'hoverB', 'hoverC']);
logs.clear();
// Move to the B only area.
await gesture.moveTo(const Offset(25, 75));
await tester.pumpAndSettle();
expect(logs, <String>['exitC']);
expect(logs, <String>['exitC', 'hoverA', 'hoverB']);
logs.clear();
// Move back to the overlapping area.
await gesture.moveTo(const Offset(75, 75));
await tester.pumpAndSettle();
expect(logs, <String>['enterC']);
expect(logs, <String>['enterC', 'hoverA', 'hoverB', 'hoverC']);
logs.clear();
// Move to the C only area.
await gesture.moveTo(const Offset(125, 75));
await tester.pumpAndSettle();
expect(logs, <String>['exitB']);
expect(logs, <String>['exitB', 'hoverA', 'hoverC']);
logs.clear();
// Move back to the overlapping area.
await gesture.moveTo(const Offset(75, 75));
await tester.pumpAndSettle();
expect(logs, <String>['enterB']);
expect(logs, <String>['enterB', 'hoverA', 'hoverB', 'hoverC']);
logs.clear();
// Move out.
......@@ -1220,31 +1226,31 @@ void main() {
// Move to the overlapping area.
await gesture.moveTo(const Offset(75, 75));
await tester.pumpAndSettle();
expect(logs, <String>['enterA', 'enterC']);
expect(logs, <String>['enterA', 'enterC', 'hoverA', 'hoverC']);
logs.clear();
// Move to the B only area.
await gesture.moveTo(const Offset(25, 75));
await tester.pumpAndSettle();
expect(logs, <String>['exitC', 'enterB']);
expect(logs, <String>['exitC', 'enterB', 'hoverA', 'hoverB']);
logs.clear();
// Move back to the overlapping area.
await gesture.moveTo(const Offset(75, 75));
await tester.pumpAndSettle();
expect(logs, <String>['exitB', 'enterC']);
expect(logs, <String>['exitB', 'enterC', 'hoverA', 'hoverC']);
logs.clear();
// Move to the C only area.
await gesture.moveTo(const Offset(125, 75));
await tester.pumpAndSettle();
expect(logs, <String>[]);
expect(logs, <String>['hoverA', 'hoverC']);
logs.clear();
// Move back to the overlapping area.
await gesture.moveTo(const Offset(75, 75));
await tester.pumpAndSettle();
expect(logs, <String>[]);
expect(logs, <String>['hoverA', 'hoverC']);
logs.clear();
// Move out.
......@@ -1268,7 +1274,7 @@ void main() {
// Move to the overlapping area.
await gesture.moveTo(const Offset(75, 75));
await tester.pumpAndSettle();
expect(logs, <String>['enterA', 'enterC']);
expect(logs, <String>['enterA', 'enterC', 'hoverA', 'hoverC']);
logs.clear();
// Move out.
......
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