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