Trigger MouseRegion.toHover only on hover events (#47403)
This commit is contained in:
parent
3ae0345e72
commit
0450e7c0ff
@ -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 {
|
|||||||
_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 {
|
|||||||
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 {
|
|||||||
// 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 {
|
|||||||
// 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 {
|
|||||||
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 {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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,118 @@ 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;
|
||||||
|
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();
|
||||||
|
|
||||||
|
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(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;
|
PointerEnterEvent enter;
|
||||||
PointerHoverEvent move;
|
PointerHoverEvent move;
|
||||||
PointerExitEvent exit;
|
PointerExitEvent exit;
|
||||||
@ -161,21 +267,105 @@ void main() {
|
|||||||
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);
|
move = null;
|
||||||
expect(move.position, equals(const Offset(400.0, 300.0)));
|
enter = null;
|
||||||
expect(enter, isNotNull);
|
exit = null;
|
||||||
expect(enter.position, equals(const Offset(400.0, 300.0)));
|
|
||||||
expect(exit, isNull);
|
|
||||||
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() {
|
|||||||
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));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user