Commit 0450e7c0 authored by Tong Mu's avatar Tong Mu Committed by Flutter GitHub Bot

Trigger MouseRegion.toHover only on hover events (#47403)

parent 3ae0345e
......@@ -289,7 +289,6 @@ class MouseTracker extends ChangeNotifier {
return;
final PointerEvent previousEvent = existingState?.latestEvent;
final Offset lastHoverPosition = previousEvent is! PointerHoverEvent ? null : previousEvent.position;
_updateDevices(
targetEvent: event,
handleUpdatedDevice: (_MouseState mouseState, LinkedHashSet<MouseTrackerAnnotation> previousAnnotations) {
......@@ -297,7 +296,7 @@ class MouseTracker extends ChangeNotifier {
_dispatchDeviceCallbacks(
lastAnnotations: previousAnnotations,
nextAnnotations: mouseState.annotations,
lastHoverPosition: lastHoverPosition,
previousEvent: previousEvent,
unhandledEvent: event,
trackedAnnotations: _trackedAnnotations,
);
......@@ -328,13 +327,11 @@ class MouseTracker extends ChangeNotifier {
void _updateAllDevices() {
_updateDevices(
handleUpdatedDevice: (_MouseState mouseState, LinkedHashSet<MouseTrackerAnnotation> previousAnnotations) {
final PointerEvent latestEvent = mouseState.latestEvent;
final Offset lastHoverPosition = latestEvent is PointerHoverEvent ? latestEvent.position : null;
_dispatchDeviceCallbacks(
lastAnnotations: previousAnnotations,
nextAnnotations: mouseState.annotations,
lastHoverPosition: lastHoverPosition,
unhandledEvent: mouseState.latestEvent,
previousEvent: mouseState.latestEvent,
unhandledEvent: null,
trackedAnnotations: _trackedAnnotations,
);
}
......@@ -427,20 +424,22 @@ class MouseTracker extends ChangeNotifier {
// Dispatch callbacks related to a device after all necessary information
// has been collected.
//
// The `lastHoverPosition` can be null, which means the last event is not a
// hover. Other arguments must not be null.
// 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.
static void _dispatchDeviceCallbacks({
@required LinkedHashSet<MouseTrackerAnnotation> lastAnnotations,
@required LinkedHashSet<MouseTrackerAnnotation> nextAnnotations,
@required Offset lastHoverPosition,
@required PointerEvent previousEvent,
@required PointerEvent unhandledEvent,
@required Set<MouseTrackerAnnotation> trackedAnnotations,
}) {
assert(lastAnnotations != null);
assert(nextAnnotations != null);
// lastHoverPosition can be null
assert(unhandledEvent != null);
assert(trackedAnnotations != 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".
......@@ -456,7 +455,7 @@ class MouseTracker extends ChangeNotifier {
// trigger may cause exceptions and has safer alternatives. See
// [MouseRegion.onExit] for details.
if (annotation.onExit != null && attached) {
annotation.onExit(PointerExitEvent.fromMouseEvent(unhandledEvent));
annotation.onExit(PointerExitEvent.fromMouseEvent(latestEvent));
}
}
......@@ -466,7 +465,7 @@ class MouseTracker extends ChangeNotifier {
for (final MouseTrackerAnnotation annotation in enteringAnnotations) {
assert(trackedAnnotations.contains(annotation));
if (annotation.onEnter != null) {
annotation.onEnter(PointerEnterEvent.fromMouseEvent(unhandledEvent));
annotation.onEnter(PointerEnterEvent.fromMouseEvent(latestEvent));
}
}
......@@ -476,6 +475,7 @@ class MouseTracker extends ChangeNotifier {
if (unhandledEvent is PointerHoverEvent) {
final Iterable<MouseTrackerAnnotation> hoveringAnnotations =
nextAnnotations.toList().reversed;
final Offset lastHoverPosition = previousEvent is PointerHoverEvent ? previousEvent.position : null;
for (final MouseTrackerAnnotation annotation in hoveringAnnotations) {
// Deduplicate: Trigger hover if it's a newly hovered annotation
// or the position has changed
......
......@@ -107,8 +107,8 @@ void main() {
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer);
await gesture.moveTo(const Offset(400.0, 300.0));
await tester.pump();
await gesture.moveTo(const Offset(400.0, 300.0));
expect(move, isNotNull);
expect(move.position, equals(const Offset(400.0, 300.0)));
expect(enter, isNotNull);
......@@ -593,8 +593,8 @@ void main() {
TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: Offset.zero);
addTearDown(() => gesture?.removePointer());
await gesture.moveTo(tester.getCenter(find.byType(Container)));
await tester.pumpAndSettle();
await gesture.moveTo(tester.getCenter(find.byType(Container)));
expect(enter.length, 1);
expect(enter.single.position, const Offset(400.0, 300.0));
......
......@@ -101,8 +101,11 @@ void main() {
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer);
await gesture.moveTo(const Offset(400.0, 300.0));
await tester.pump();
move = null;
enter = null;
exit = null;
await gesture.moveTo(const Offset(400.0, 300.0));
expect(move, isNotNull);
expect(move.position, equals(const Offset(400.0, 300.0)));
expect(enter, isNotNull);
......@@ -132,15 +135,15 @@ void main() {
await tester.pump();
move = null;
enter = null;
exit = null;
await gesture.moveTo(const Offset(1.0, 1.0));
await tester.pump();
expect(move, isNull);
expect(enter, isNull);
expect(exit, isNotNull);
expect(exit.position, equals(const Offset(1.0, 1.0)));
});
testWidgets('detects pointer exit when widget disappears', (WidgetTester tester) async {
testWidgets('triggers pointer enter when a mouse is connected', (WidgetTester tester) async {
PointerEnterEvent enter;
PointerHoverEvent move;
PointerExitEvent exit;
......@@ -155,27 +158,214 @@ void main() {
onExit: (PointerExitEvent details) => exit = details,
),
));
final RenderMouseRegion renderListener = tester.renderObject(find.byType(MouseRegion));
await tester.pump();
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: const Offset(400, 300));
addTearDown(gesture.removePointer);
expect(move, isNull);
expect(enter, isNull);
expect(exit, isNull);
await tester.pump();
expect(move, isNull);
expect(enter, isNotNull);
expect(enter.position, equals(const Offset(400.0, 300.0)));
expect(exit, isNull);
});
testWidgets('triggers pointer exit when a mouse is disconnected', (WidgetTester tester) async {
PointerEnterEvent enter;
PointerHoverEvent move;
PointerExitEvent exit;
await tester.pumpWidget(Center(
child: MouseRegion(
child: Container(
width: 100.0,
height: 100.0,
),
onEnter: (PointerEnterEvent details) => enter = details,
onHover: (PointerHoverEvent details) => move = details,
onExit: (PointerExitEvent details) => exit = details,
),
));
await tester.pump();
TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: const Offset(400, 300));
addTearDown(() => gesture?.removePointer);
await tester.pump();
move = null;
enter = null;
exit = null;
await gesture.removePointer();
gesture = null;
expect(move, isNull);
expect(enter, isNull);
expect(exit, isNotNull);
expect(exit.position, equals(const Offset(400.0, 300.0)));
exit = null;
await tester.pump();
expect(move, isNull);
expect(enter, isNull);
expect(exit, isNull);
});
testWidgets('triggers pointer enter when widget appears', (WidgetTester tester) async {
PointerEnterEvent enter;
PointerHoverEvent move;
PointerExitEvent exit;
await tester.pumpWidget(Center(
child: Container(
width: 100.0,
height: 100.0,
),
));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer);
await gesture.moveTo(const Offset(400.0, 300.0));
await tester.pump();
expect(move, isNotNull);
expect(move.position, equals(const Offset(400.0, 300.0)));
expect(enter, isNull);
expect(move, isNull);
expect(exit, isNull);
await tester.pumpWidget(Center(
child: MouseRegion(
child: Container(
width: 100.0,
height: 100.0,
),
onEnter: (PointerEnterEvent details) => enter = details,
onHover: (PointerHoverEvent details) => move = details,
onExit: (PointerExitEvent details) => exit = details,
),
));
await tester.pump();
expect(move, isNull);
expect(enter, isNotNull);
expect(enter.position, equals(const Offset(400.0, 300.0)));
expect(exit, isNull);
});
testWidgets("doesn't trigger pointer exit when widget disappears", (WidgetTester tester) async {
PointerEnterEvent enter;
PointerHoverEvent move;
PointerExitEvent exit;
await tester.pumpWidget(Center(
child: MouseRegion(
child: Container(
width: 100.0,
height: 100.0,
),
onEnter: (PointerEnterEvent details) => enter = details,
onHover: (PointerHoverEvent details) => move = details,
onExit: (PointerExitEvent details) => exit = details,
),
));
final RenderMouseRegion renderListener = tester.renderObject(find.byType(MouseRegion));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer);
await gesture.moveTo(const Offset(400.0, 300.0));
await tester.pump();
move = null;
enter = null;
exit = null;
await tester.pumpWidget(Center(
child: Container(
width: 100.0,
height: 100.0,
),
));
expect(enter, isNull);
expect(move, isNull);
expect(exit, isNull);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener.hoverAnnotation), isFalse);
});
testWidgets('triggers pointer enter when widget moves in', (WidgetTester tester) async {
PointerEnterEvent enter;
PointerHoverEvent move;
PointerExitEvent exit;
await tester.pumpWidget(Container(
alignment: Alignment.center,
child: MouseRegion(
child: Container(
width: 100.0,
height: 100.0,
),
onEnter: (PointerEnterEvent details) => enter = details,
onHover: (PointerHoverEvent details) => move = details,
onExit: (PointerExitEvent details) => exit = details,
),
));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: const Offset(1.0, 1.0));
addTearDown(gesture.removePointer);
await tester.pump();
expect(enter, isNull);
expect(move, isNull);
expect(exit, isNull);
await tester.pumpWidget(Container(
alignment: Alignment.topLeft,
child: MouseRegion(
child: Container(
width: 100.0,
height: 100.0,
),
onEnter: (PointerEnterEvent details) => enter = details,
onHover: (PointerHoverEvent details) => move = details,
onExit: (PointerExitEvent details) => exit = details,
),
));
await tester.pump();
expect(enter, isNotNull);
expect(enter.position, equals(const Offset(1.0, 1.0)));
expect(move, isNull);
expect(exit, isNull);
});
testWidgets('triggers pointer exit when widget moves out', (WidgetTester tester) async {
PointerEnterEvent enter;
PointerHoverEvent move;
PointerExitEvent exit;
await tester.pumpWidget(Container(
alignment: Alignment.center,
child: MouseRegion(
child: Container(
width: 100.0,
height: 100.0,
),
onEnter: (PointerEnterEvent details) => enter = details,
onHover: (PointerHoverEvent details) => move = details,
onExit: (PointerExitEvent details) => exit = details,
),
));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: const Offset(400, 300));
addTearDown(gesture.removePointer);
await tester.pump();
enter = null;
move = null;
exit = null;
await tester.pumpWidget(Container(
alignment: Alignment.topLeft,
child: MouseRegion(
child: Container(
width: 100.0,
height: 100.0,
),
onEnter: (PointerEnterEvent details) => enter = details,
onHover: (PointerHoverEvent details) => move = details,
onExit: (PointerExitEvent details) => exit = details,
),
));
await tester.pump();
expect(enter, isNull);
expect(move, isNull);
expect(exit, isNotNull);
expect(exit.position, equals(const Offset(400, 300)));
});
testWidgets('Hover works with nested listeners', (WidgetTester tester) async {
final UniqueKey key1 = UniqueKey();
final UniqueKey key2 = UniqueKey();
......@@ -680,8 +870,8 @@ void main() {
TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: Offset.zero);
addTearDown(() => gesture?.removePointer());
await gesture.moveTo(tester.getCenter(find.byType(Container)));
await tester.pumpAndSettle();
await gesture.moveTo(tester.getCenter(find.byType(Container)));
expect(enter.length, 1);
expect(enter.single.position, const Offset(400.0, 300.0));
......
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