Unverified Commit 05097916 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Fix mouse hover to not schedule a frame for every mouse move. (#41014)

This fixes the mouse hover code to not schedule frames with every mouse move.

Before this, it would schedule a post frame callback, and then schedule a frame immediately, even if there was nothing that needed to be updated. Now it will schedule checks for mouse position updates synchronously, unless there's a new annotation, and skip scheduling a new frame in all cases. It has to be async in the case of a new annotation (i.e. a new MouseRegion is added), since when the annotation is added, it hasn't yet painted, and it can't hit test against the new layer until after the paint, so in that case it schedules a post frame callback, but since it's already building a frame when it does that, it doesn't need to schedule a frame.

The code also used to do mouse position checks for all mice if only one mouse changed position. I fixed this part too, so that it will only check position for the mouse that changed.
parent 75552684
...@@ -92,12 +92,21 @@ class MouseTracker extends ChangeNotifier { ...@@ -92,12 +92,21 @@ class MouseTracker extends ChangeNotifier {
/// Creates a mouse tracker to keep track of mouse locations. /// Creates a mouse tracker to keep track of mouse locations.
/// ///
/// All of the parameters must not be null. /// All of the parameters must not be null.
MouseTracker(PointerRouter router, this.annotationFinder) MouseTracker(this._router, this.annotationFinder)
: assert(router != null), : assert(_router != null),
assert(annotationFinder != null) { assert(annotationFinder != null) {
router.addGlobalRoute(_handleEvent); _router.addGlobalRoute(_handleEvent);
} }
@override
void dispose() {
super.dispose();
_router.removeGlobalRoute(_handleEvent);
}
// The pointer router that the mouse tracker listens to for events.
final PointerRouter _router;
/// Used to find annotations at a given logical coordinate. /// Used to find annotations at a given logical coordinate.
final MouseDetectorAnnotationFinder annotationFinder; final MouseDetectorAnnotationFinder annotationFinder;
...@@ -111,16 +120,19 @@ class MouseTracker extends ChangeNotifier { ...@@ -111,16 +120,19 @@ class MouseTracker extends ChangeNotifier {
/// annotation has been added to the layer tree. /// annotation has been added to the layer tree.
void attachAnnotation(MouseTrackerAnnotation annotation) { void attachAnnotation(MouseTrackerAnnotation annotation) {
_trackedAnnotations[annotation] = _TrackedAnnotation(annotation); _trackedAnnotations[annotation] = _TrackedAnnotation(annotation);
// Schedule a check so that we test this new annotation to see if the mouse // Schedule a check so that we test this new annotation to see if any mouse
// is currently inside its region. // is currently inside its region. It has to happen after the frame is
_scheduleMousePositionCheck(); // complete so that the annotation layer has been added before the check.
if (mouseIsConnected) {
_scheduleMousePositionCheck();
}
} }
/// Stops tracking an annotation, indicating that it has been removed from the /// Stops tracking an annotation, indicating that it has been removed from the
/// layer tree. /// layer tree.
/// ///
/// If the associated layer is not removed, and receives a hit, then /// If the associated layer is not removed, and receives a hit, then
/// [collectMousePositions] will assert the next time it is called. /// [sendMouseNotifications] will assert the next time it is called.
void detachAnnotation(MouseTrackerAnnotation annotation) { void detachAnnotation(MouseTrackerAnnotation annotation) {
final _TrackedAnnotation trackedAnnotation = _findAnnotation(annotation); final _TrackedAnnotation trackedAnnotation = _findAnnotation(annotation);
for (int deviceId in trackedAnnotation.activeDevices) { for (int deviceId in trackedAnnotation.activeDevices) {
...@@ -133,18 +145,19 @@ class MouseTracker extends ChangeNotifier { ...@@ -133,18 +145,19 @@ class MouseTracker extends ChangeNotifier {
_trackedAnnotations.remove(annotation); _trackedAnnotations.remove(annotation);
} }
bool _postFrameCheckScheduled = false; bool _scheduledPostFramePositionCheck = false;
// Schedules a position check at the end of this frame for those annotations
// that have been added.
void _scheduleMousePositionCheck() { void _scheduleMousePositionCheck() {
// If we're not tracking anything, then there is no point in registering a // If we're not tracking anything, then there is no point in registering a
// frame callback or scheduling a frame. By definition there are no active // frame callback or scheduling a frame. By definition there are no active
// annotations that need exiting, either. // annotations that need exiting, either.
if (_trackedAnnotations.isNotEmpty && !_postFrameCheckScheduled) { if (_trackedAnnotations.isNotEmpty && !_scheduledPostFramePositionCheck) {
_postFrameCheckScheduled = true; _scheduledPostFramePositionCheck = true;
SchedulerBinding.instance.addPostFrameCallback((Duration _) { SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
_postFrameCheckScheduled = false; sendMouseNotifications(_lastMouseEvent.keys);
collectMousePositions(); _scheduledPostFramePositionCheck = false;
}); });
SchedulerBinding.instance.scheduleFrame();
} }
} }
...@@ -158,21 +171,24 @@ class MouseTracker extends ChangeNotifier { ...@@ -158,21 +171,24 @@ class MouseTracker extends ChangeNotifier {
// If we are adding the device again, then we're not removing it anymore. // If we are adding the device again, then we're not removing it anymore.
_pendingRemovals.remove(deviceId); _pendingRemovals.remove(deviceId);
_addMouseEvent(deviceId, event); _addMouseEvent(deviceId, event);
sendMouseNotifications(<int>{deviceId});
return; return;
} }
if (event is PointerRemovedEvent) { if (event is PointerRemovedEvent) {
_removeMouseEvent(deviceId, event); _removeMouseEvent(deviceId, event);
// If the mouse was removed, then we need to schedule one more check to // If the mouse was removed, then we need to schedule one more check to
// exit any annotations that were active. // exit any annotations that were active.
_scheduleMousePositionCheck(); sendMouseNotifications(<int>{deviceId});
} else { } else {
if (event is PointerMoveEvent || event is PointerHoverEvent || event is PointerDownEvent) { if (event is PointerMoveEvent || event is PointerHoverEvent || event is PointerDownEvent) {
if (!_lastMouseEvent.containsKey(deviceId) || _lastMouseEvent[deviceId].position != event.position) { final PointerEvent lastEvent = _lastMouseEvent[deviceId];
_addMouseEvent(deviceId, event);
if (lastEvent == null ||
lastEvent is PointerAddedEvent || lastEvent.position != event.position) {
// Only schedule a frame if we have our first event, or if the // Only schedule a frame if we have our first event, or if the
// location of the mouse has changed, and only if there are tracked annotations. // location of the mouse has changed, and only if there are tracked annotations.
_scheduleMousePositionCheck(); sendMouseNotifications(<int>{deviceId});
} }
_addMouseEvent(deviceId, event);
} }
} }
} }
...@@ -206,14 +222,18 @@ class MouseTracker extends ChangeNotifier { ...@@ -206,14 +222,18 @@ class MouseTracker extends ChangeNotifier {
/// This function is only public to allow for proper testing of the /// This function is only public to allow for proper testing of the
/// MouseTracker. Do not call in other contexts. /// MouseTracker. Do not call in other contexts.
@visibleForTesting @visibleForTesting
void collectMousePositions() { void sendMouseNotifications(Iterable<int> deviceIds) {
if (_trackedAnnotations.isEmpty) {
return;
}
void exitAnnotation(_TrackedAnnotation trackedAnnotation, int deviceId) { void exitAnnotation(_TrackedAnnotation trackedAnnotation, int deviceId) {
if (trackedAnnotation.annotation?.onExit != null && trackedAnnotation.activeDevices.contains(deviceId)) { if (trackedAnnotation.annotation?.onExit != null && trackedAnnotation.activeDevices.contains(deviceId)) {
final PointerEvent event = _lastMouseEvent[deviceId] ?? _pendingRemovals[deviceId]; final PointerEvent event = _lastMouseEvent[deviceId] ?? _pendingRemovals[deviceId];
assert(event != null); assert(event != null);
trackedAnnotation.annotation.onExit(PointerExitEvent.fromMouseEvent(event)); trackedAnnotation.annotation.onExit(PointerExitEvent.fromMouseEvent(event));
trackedAnnotation.activeDevices.remove(deviceId);
} }
trackedAnnotation.activeDevices.remove(deviceId);
} }
void exitAllDevices(_TrackedAnnotation trackedAnnotation) { void exitAllDevices(_TrackedAnnotation trackedAnnotation) {
...@@ -234,8 +254,9 @@ class MouseTracker extends ChangeNotifier { ...@@ -234,8 +254,9 @@ class MouseTracker extends ChangeNotifier {
return; return;
} }
for (int deviceId in _lastMouseEvent.keys) { for (int deviceId in deviceIds) {
final PointerEvent lastEvent = _lastMouseEvent[deviceId]; final PointerEvent lastEvent = _lastMouseEvent[deviceId];
assert(lastEvent != null);
final Iterable<MouseTrackerAnnotation> hits = annotationFinder(lastEvent.position); final Iterable<MouseTrackerAnnotation> hits = annotationFinder(lastEvent.position);
// No annotations were found at this position for this deviceId, so send an // No annotations were found at this position for this deviceId, so send an
...@@ -311,7 +332,6 @@ class MouseTracker extends ChangeNotifier { ...@@ -311,7 +332,6 @@ class MouseTracker extends ChangeNotifier {
/// The most recent mouse event observed for each mouse device ID observed. /// The most recent mouse event observed for each mouse device ID observed.
/// ///
/// May be null if no mouse is connected, or hasn't produced an event yet. /// May be null if no mouse is connected, or hasn't produced an event yet.
/// Will not be updated unless there is at least one tracked annotation.
final Map<int, PointerEvent> _lastMouseEvent = <int, PointerEvent>{}; final Map<int, PointerEvent> _lastMouseEvent = <int, PointerEvent>{};
/// Whether or not a mouse is connected and has produced events. /// Whether or not a mouse is connected and has produced events.
......
...@@ -43,7 +43,7 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture ...@@ -43,7 +43,7 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
_handleSemanticsEnabledChanged(); _handleSemanticsEnabledChanged();
assert(renderView != null); assert(renderView != null);
addPersistentFrameCallback(_handlePersistentFrameCallback); addPersistentFrameCallback(_handlePersistentFrameCallback);
_mouseTracker = _createMouseTracker(); initMouseTracker();
} }
/// The current [RendererBinding], if one has been created. /// The current [RendererBinding], if one has been created.
...@@ -238,10 +238,14 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture ...@@ -238,10 +238,14 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
SemanticsHandle _semanticsHandle; SemanticsHandle _semanticsHandle;
// Creates a [MouseTracker] which manages state about currently connected /// Creates a [MouseTracker] which manages state about currently connected
// mice, for hover notification. /// mice, for hover notification.
MouseTracker _createMouseTracker() { ///
return MouseTracker(pointerRouter, renderView.hitTestMouseTrackers); /// Used by testing framework to reinitialize the mouse tracker between tests.
@visibleForTesting
void initMouseTracker([MouseTracker tracker]) {
_mouseTracker?.dispose();
_mouseTracker = tracker ?? MouseTracker(pointerRouter, renderView.hitTestMouseTrackers);
} }
void _handleSemanticsEnabledChanged() { void _handleSemanticsEnabledChanged() {
......
...@@ -5833,7 +5833,7 @@ class MouseRegion extends SingleChildRenderObjectWidget { ...@@ -5833,7 +5833,7 @@ class MouseRegion extends SingleChildRenderObjectWidget {
final PointerExitEventListener onExit; final PointerExitEventListener onExit;
@override @override
_ListenerElement createElement() => _ListenerElement(this); _MouseRegionElement createElement() => _MouseRegionElement(this);
@override @override
RenderMouseRegion createRenderObject(BuildContext context) { RenderMouseRegion createRenderObject(BuildContext context) {
...@@ -5866,20 +5866,20 @@ class MouseRegion extends SingleChildRenderObjectWidget { ...@@ -5866,20 +5866,20 @@ class MouseRegion extends SingleChildRenderObjectWidget {
} }
} }
class _ListenerElement extends SingleChildRenderObjectElement { class _MouseRegionElement extends SingleChildRenderObjectElement {
_ListenerElement(SingleChildRenderObjectWidget widget) : super(widget); _MouseRegionElement(SingleChildRenderObjectWidget widget) : super(widget);
@override @override
void activate() { void activate() {
super.activate(); super.activate();
final RenderMouseRegion renderMouseListener = renderObject; final RenderMouseRegion renderMouseRegion = renderObject;
renderMouseListener.postActivate(); renderMouseRegion.postActivate();
} }
@override @override
void deactivate() { void deactivate() {
final RenderMouseRegion renderMouseListener = renderObject; final RenderMouseRegion renderMouseRegion = renderObject;
renderMouseListener.preDeactivate(); renderMouseRegion.preDeactivate();
super.deactivate(); super.deactivate();
} }
} }
......
...@@ -4,7 +4,9 @@ ...@@ -4,7 +4,9 @@
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
...@@ -13,7 +15,7 @@ import '../flutter_test_alternative.dart'; ...@@ -13,7 +15,7 @@ import '../flutter_test_alternative.dart';
typedef HandleEventCallback = void Function(PointerEvent event); typedef HandleEventCallback = void Function(PointerEvent event);
class TestGestureFlutterBinding extends BindingBase with ServicesBinding, SchedulerBinding, GestureBinding { class TestGestureFlutterBinding extends BindingBase with ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, RendererBinding {
HandleEventCallback callback; HandleEventCallback callback;
@override @override
...@@ -35,321 +37,335 @@ void ensureTestGestureBinding() { ...@@ -35,321 +37,335 @@ void ensureTestGestureBinding() {
void main() { void main() {
setUp(ensureTestGestureBinding); setUp(ensureTestGestureBinding);
group(MouseTracker, () { final List<PointerEnterEvent> enter = <PointerEnterEvent>[];
final List<PointerEnterEvent> enter = <PointerEnterEvent>[]; final List<PointerHoverEvent> move = <PointerHoverEvent>[];
final List<PointerHoverEvent> move = <PointerHoverEvent>[]; final List<PointerExitEvent> exit = <PointerExitEvent>[];
final List<PointerExitEvent> exit = <PointerExitEvent>[]; final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
final MouseTrackerAnnotation annotation = MouseTrackerAnnotation( onEnter: (PointerEnterEvent event) => enter.add(event),
onEnter: (PointerEnterEvent event) => enter.add(event), onHover: (PointerHoverEvent event) => move.add(event),
onHover: (PointerHoverEvent event) => move.add(event), onExit: (PointerExitEvent event) => exit.add(event),
onExit: (PointerExitEvent event) => exit.add(event), );
); // Only respond to some mouse events.
// Only respond to some mouse events. final MouseTrackerAnnotation partialAnnotation = MouseTrackerAnnotation(
final MouseTrackerAnnotation partialAnnotation = MouseTrackerAnnotation( onEnter: (PointerEnterEvent event) => enter.add(event),
onEnter: (PointerEnterEvent event) => enter.add(event), onHover: (PointerHoverEvent event) => move.add(event),
onHover: (PointerHoverEvent event) => move.add(event), );
); bool isInHitRegionOne;
bool isInHitRegionOne; bool isInHitRegionTwo;
bool isInHitRegionTwo;
MouseTracker tracker;
void clear() { void clear() {
enter.clear(); enter.clear();
exit.clear(); exit.clear();
move.clear(); move.clear();
} }
setUp(() { setUp(() {
clear(); clear();
isInHitRegionOne = true; isInHitRegionOne = true;
isInHitRegionTwo = false; isInHitRegionTwo = false;
tracker = MouseTracker( RendererBinding.instance.initMouseTracker(
MouseTracker(
GestureBinding.instance.pointerRouter, GestureBinding.instance.pointerRouter,
(Offset _) sync* { (Offset position) sync* {
if (isInHitRegionOne) if (isInHitRegionOne)
yield annotation; yield annotation;
else if (isInHitRegionTwo) else if (isInHitRegionTwo) {
yield partialAnnotation; yield partialAnnotation;
}
}, },
); ),
}); );
PointerEventConverter.clearPointers();
});
test('receives and processes mouse hover events', () {
final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.hover, // Will implicitly also add a PointerAdded event.
physicalX: 0.0 * ui.window.devicePixelRatio,
physicalY: 0.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
]);
final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 101.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
]);
final ui.PointerDataPacket packet3 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.remove,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 201.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
]);
final ui.PointerDataPacket packet4 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 301.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
]);
final ui.PointerDataPacket packet5 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 401.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
device: 1,
),
]);
RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
RendererBinding.instance.mouseTracker.sendMouseNotifications(<int>{0});
isInHitRegionOne = true;
ui.window.onPointerDataPacket(packet1);
expect(enter.length, equals(1), reason: 'enter contains $enter');
expect(enter.first.position, equals(const Offset(0.0, 0.0)));
expect(enter.first.device, equals(0));
expect(enter.first.runtimeType, equals(PointerEnterEvent));
expect(exit.length, equals(0), reason: 'exit contains $exit');
expect(move.length, equals(1), reason: 'move contains $move');
expect(move.first.position, equals(const Offset(0.0, 0.0)));
expect(move.first.device, equals(0));
expect(move.first.runtimeType, equals(PointerHoverEvent));
clear();
test('receives and processes mouse hover events', () { ui.window.onPointerDataPacket(packet2);
final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[ expect(enter.length, equals(0), reason: 'enter contains $enter');
ui.PointerData( expect(exit.length, equals(0), reason: 'exit contains $exit');
change: ui.PointerChange.hover, expect(move.length, equals(1), reason: 'move contains $move');
physicalX: 0.0 * ui.window.devicePixelRatio, expect(move.first.position, equals(const Offset(1.0, 101.0)));
physicalY: 0.0 * ui.window.devicePixelRatio, expect(move.first.device, equals(0));
kind: PointerDeviceKind.mouse, expect(move.first.runtimeType, equals(PointerHoverEvent));
), clear();
]);
final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 101.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
]);
final ui.PointerDataPacket packet3 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.remove,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 201.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
]);
final ui.PointerDataPacket packet4 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 301.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
]);
final ui.PointerDataPacket packet5 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 401.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
device: 1,
),
]);
tracker.attachAnnotation(annotation);
isInHitRegionOne = true;
ui.window.onPointerDataPacket(packet1);
tracker.collectMousePositions();
expect(enter.length, equals(1), reason: 'enter contains $enter');
expect(enter.first.position, equals(const Offset(0.0, 0.0)));
expect(enter.first.device, equals(0));
expect(enter.first.runtimeType, equals(PointerEnterEvent));
expect(exit.length, equals(0), reason: 'exit contains $exit');
expect(move.length, equals(1), reason: 'move contains $move');
expect(move.first.position, equals(const Offset(0.0, 0.0)));
expect(move.first.device, equals(0));
expect(move.first.runtimeType, equals(PointerHoverEvent));
clear();
ui.window.onPointerDataPacket(packet2); ui.window.onPointerDataPacket(packet3);
tracker.collectMousePositions(); expect(enter.length, equals(0), reason: 'enter contains $enter');
expect(enter.length, equals(0), reason: 'enter contains $enter'); expect(move.length, equals(1), reason: 'move contains $move');
expect(exit.length, equals(0), reason: 'exit contains $exit'); expect(move.first.position, equals(const Offset(1.0, 201.0)));
expect(move.length, equals(1), reason: 'move contains $move'); expect(move.first.device, equals(0));
expect(move.first.position, equals(const Offset(1.0, 101.0))); expect(move.first.runtimeType, equals(PointerHoverEvent));
expect(move.first.device, equals(0)); expect(exit.length, equals(1), reason: 'exit contains $exit');
expect(move.first.runtimeType, equals(PointerHoverEvent)); expect(exit.first.position, equals(const Offset(1.0, 201.0)));
clear(); expect(exit.first.device, equals(0));
expect(exit.first.runtimeType, equals(PointerExitEvent));
ui.window.onPointerDataPacket(packet3); clear();
tracker.collectMousePositions(); ui.window.onPointerDataPacket(packet4);
expect(enter.length, equals(0), reason: 'enter contains $enter'); expect(enter.length, equals(1), reason: 'enter contains $enter');
expect(move.length, equals(0), reason: 'move contains $move'); expect(enter.first.position, equals(const Offset(1.0, 301.0)));
expect(exit.length, equals(1), reason: 'exit contains $exit'); expect(enter.first.device, equals(0));
expect(exit.first.position, equals(const Offset(1.0, 201.0))); expect(enter.first.runtimeType, equals(PointerEnterEvent));
expect(exit.first.device, equals(0)); expect(exit.length, equals(0), reason: 'exit contains $exit');
expect(exit.first.runtimeType, equals(PointerExitEvent)); expect(move.length, equals(1), reason: 'move contains $move');
expect(move.first.position, equals(const Offset(1.0, 301.0)));
expect(move.first.device, equals(0));
expect(move.first.runtimeType, equals(PointerHoverEvent));
clear(); // add in a second mouse simultaneously.
ui.window.onPointerDataPacket(packet4); clear();
tracker.collectMousePositions(); ui.window.onPointerDataPacket(packet5);
expect(enter.length, equals(1), reason: 'enter contains $enter'); RendererBinding.instance.mouseTracker.sendMouseNotifications(<int>{1});
expect(enter.first.position, equals(const Offset(1.0, 301.0))); expect(enter.length, equals(1), reason: 'enter contains $enter');
expect(enter.first.device, equals(0)); expect(enter.first.position, equals(const Offset(1.0, 401.0)));
expect(enter.first.runtimeType, equals(PointerEnterEvent)); expect(enter.first.device, equals(1));
expect(exit.length, equals(0), reason: 'exit contains $exit'); expect(enter.first.runtimeType, equals(PointerEnterEvent));
expect(move.length, equals(1), reason: 'move contains $move'); expect(exit.length, equals(0), reason: 'exit contains $exit');
expect(move.first.position, equals(const Offset(1.0, 301.0))); expect(move.length, equals(2), reason: 'move contains $move');
expect(move.first.device, equals(0)); expect(move.first.position, equals(const Offset(1.0, 401.0)));
expect(move.first.runtimeType, equals(PointerHoverEvent)); expect(move.first.device, equals(1));
expect(move.first.runtimeType, equals(PointerHoverEvent));
expect(move.last.position, equals(const Offset(1.0, 401.0)));
expect(move.last.device, equals(1));
expect(move.last.runtimeType, equals(PointerHoverEvent));
});
test('detects exit when annotated layer no longer hit', () {
final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 0.0 * ui.window.devicePixelRatio,
physicalY: 0.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 101.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
]);
final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 201.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
]);
isInHitRegionOne = true;
RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
RendererBinding.instance.mouseTracker.sendMouseNotifications(<int>{0});
// add in a second mouse simultaneously. ui.window.onPointerDataPacket(packet1);
clear();
ui.window.onPointerDataPacket(packet5);
tracker.collectMousePositions();
expect(enter.length, equals(1), reason: 'enter contains $enter');
expect(enter.first.position, equals(const Offset(1.0, 401.0)));
expect(enter.first.device, equals(1));
expect(enter.first.runtimeType, equals(PointerEnterEvent));
expect(exit.length, equals(0), reason: 'exit contains $exit');
expect(move.length, equals(2), reason: 'move contains $move');
expect(move.first.position, equals(const Offset(1.0, 301.0)));
expect(move.first.device, equals(0));
expect(move.first.runtimeType, equals(PointerHoverEvent));
expect(move.last.position, equals(const Offset(1.0, 401.0)));
expect(move.last.device, equals(1));
expect(move.last.runtimeType, equals(PointerHoverEvent));
});
test('detects exit when annotated layer no longer hit', () {
final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 0.0 * ui.window.devicePixelRatio,
physicalY: 0.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 101.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
]);
final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 201.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
]);
isInHitRegionOne = true;
tracker.attachAnnotation(annotation);
ui.window.onPointerDataPacket(packet1); expect(enter.length, equals(1), reason: 'enter contains $enter');
tracker.collectMousePositions(); expect(enter.first.position, equals(const Offset(0.0, 0.0)));
expect(enter.length, equals(1), reason: 'enter contains $enter'); expect(enter.first.device, equals(0));
expect(enter.first.position, equals(const Offset(1.0, 101.0))); expect(enter.first.runtimeType, equals(PointerEnterEvent));
expect(enter.first.device, equals(0)); expect(move.length, equals(2), reason: 'move contains $move');
expect(enter.first.runtimeType, equals(PointerEnterEvent)); expect(move.first.position, equals(const Offset(0.0, 0.0)));
expect(move.length, equals(1), reason: 'move contains $move'); expect(move.first.device, equals(0));
expect(move.first.position, equals(const Offset(1.0, 101.0))); expect(move.first.runtimeType, equals(PointerHoverEvent));
expect(move.first.device, equals(0)); expect(move.last.position, equals(const Offset(1.0, 101.0)));
expect(move.first.runtimeType, equals(PointerHoverEvent)); expect(move.last.device, equals(0));
expect(exit.length, equals(0), reason: 'exit contains $exit'); expect(move.last.runtimeType, equals(PointerHoverEvent));
// Simulate layer going away by detaching it. expect(exit.length, equals(0), reason: 'exit contains $exit');
clear(); // Simulate layer going away by detaching it.
isInHitRegionOne = false; clear();
isInHitRegionOne = false;
ui.window.onPointerDataPacket(packet2); ui.window.onPointerDataPacket(packet2);
tracker.collectMousePositions(); expect(enter.length, equals(0), reason: 'enter contains $enter');
expect(enter.length, equals(0), reason: 'enter contains $enter'); expect(move.length, equals(0), reason: 'enter contains $move');
expect(move.length, equals(0), reason: 'enter contains $move'); expect(exit.length, equals(1), reason: 'enter contains $exit');
expect(exit.length, equals(1), reason: 'enter contains $exit'); expect(exit.first.position, const Offset(1.0, 201.0));
expect(exit.first.position, const Offset(1.0, 201.0)); expect(exit.first.device, equals(0));
expect(exit.first.device, equals(0)); expect(exit.first.runtimeType, equals(PointerExitEvent));
expect(exit.first.runtimeType, equals(PointerExitEvent));
// Actually detach annotation. Shouldn't receive hit. // Actually detach annotation. Shouldn't receive hit.
tracker.detachAnnotation(annotation); RendererBinding.instance.mouseTracker.detachAnnotation(annotation);
clear(); clear();
isInHitRegionOne = false; isInHitRegionOne = false;
ui.window.onPointerDataPacket(packet2);
expect(enter.length, equals(0), reason: 'enter contains $enter');
expect(move.length, equals(0), reason: 'enter contains $move');
expect(exit.length, equals(0), reason: 'enter contains $exit');
});
ui.window.onPointerDataPacket(packet2); test("don't flip out if not all mouse events are listened to", () {
tracker.collectMousePositions(); final ui.PointerDataPacket packet = ui.PointerDataPacket(data: <ui.PointerData>[
expect(enter.length, equals(0), reason: 'enter contains $enter'); ui.PointerData(
expect(move.length, equals(0), reason: 'enter contains $move'); change: ui.PointerChange.hover,
expect(exit.length, equals(0), reason: 'enter contains $exit'); physicalX: 1.0 * ui.window.devicePixelRatio,
}); physicalY: 101.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
]);
test("don't flip out if not all mouse events are listened to", () { isInHitRegionOne = false;
final ui.PointerDataPacket packet = ui.PointerDataPacket(data: <ui.PointerData>[ isInHitRegionTwo = true;
ui.PointerData( RendererBinding.instance.mouseTracker.attachAnnotation(partialAnnotation);
change: ui.PointerChange.hover,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 101.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
]);
isInHitRegionOne = false; ui.window.onPointerDataPacket(packet);
isInHitRegionTwo = true; RendererBinding.instance.mouseTracker.detachAnnotation(partialAnnotation);
tracker.attachAnnotation(partialAnnotation); isInHitRegionTwo = false;
});
test('detects exit when mouse goes away', () {
final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 0.0 * ui.window.devicePixelRatio,
physicalY: 0.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 101.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
]);
final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.remove,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 201.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
]);
isInHitRegionOne = true;
RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
RendererBinding.instance.mouseTracker.sendMouseNotifications(<int>{0});
ui.window.onPointerDataPacket(packet1);
ui.window.onPointerDataPacket(packet2);
expect(enter.length, equals(1), reason: 'enter contains $enter');
expect(enter.first.position, equals(const Offset(0.0, 0.0)));
expect(enter.first.delta, equals(const Offset(0.0, 0.0)));
expect(enter.first.device, equals(0));
expect(enter.first.runtimeType, equals(PointerEnterEvent));
expect(move.length, equals(3), reason: 'move contains $move');
expect(move[0].position, equals(const Offset(0.0, 0.0)));
expect(move[0].delta, equals(const Offset(0.0, 0.0)));
expect(move[0].device, equals(0));
expect(move[0].runtimeType, equals(PointerHoverEvent));
expect(move[1].position, equals(const Offset(1.0, 101.0)));
expect(move[1].delta, equals(const Offset(1.0, 101.0)));
expect(move[1].device, equals(0));
expect(move[1].runtimeType, equals(PointerHoverEvent));
expect(move[2].position, equals(const Offset(1.0, 201.0)));
expect(move[2].delta, equals(const Offset(0.0, 100.0)));
expect(move[2].device, equals(0));
expect(move[2].runtimeType, equals(PointerHoverEvent));
expect(exit.length, equals(1), reason: 'exit contains $exit');
expect(exit.first.position, equals(const Offset(1.0, 201.0)));
expect(exit.first.delta, equals(const Offset(0.0, 0.0)));
expect(exit.first.device, equals(0));
expect(exit.first.runtimeType, equals(PointerExitEvent));
});
ui.window.onPointerDataPacket(packet); test('handles mouse down and move', () {
tracker.collectMousePositions(); final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[
tracker.detachAnnotation(partialAnnotation); ui.PointerData(
isInHitRegionTwo = false; change: ui.PointerChange.hover,
}); physicalX: 0.0 * ui.window.devicePixelRatio,
test('detects exit when mouse goes away', () { physicalY: 0.0 * ui.window.devicePixelRatio,
final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[ kind: PointerDeviceKind.mouse,
ui.PointerData( ),
change: ui.PointerChange.hover, ui.PointerData(
physicalX: 0.0 * ui.window.devicePixelRatio, change: ui.PointerChange.hover,
physicalY: 0.0 * ui.window.devicePixelRatio, physicalX: 1.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse, physicalY: 101.0 * ui.window.devicePixelRatio,
), kind: PointerDeviceKind.mouse,
ui.PointerData( ),
change: ui.PointerChange.hover, ]);
physicalX: 1.0 * ui.window.devicePixelRatio, final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[
physicalY: 101.0 * ui.window.devicePixelRatio, ui.PointerData(
kind: PointerDeviceKind.mouse, change: ui.PointerChange.down,
), physicalX: 1.0 * ui.window.devicePixelRatio,
]); physicalY: 101.0 * ui.window.devicePixelRatio,
final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[ kind: PointerDeviceKind.mouse,
ui.PointerData( ),
change: ui.PointerChange.remove, ui.PointerData(
physicalX: 1.0 * ui.window.devicePixelRatio, change: ui.PointerChange.move,
physicalY: 201.0 * ui.window.devicePixelRatio, physicalX: 1.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse, physicalY: 201.0 * ui.window.devicePixelRatio,
), kind: PointerDeviceKind.mouse,
]); ),
isInHitRegionOne = true; ]);
tracker.attachAnnotation(annotation); isInHitRegionOne = true;
ui.window.onPointerDataPacket(packet1); RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
tracker.collectMousePositions(); RendererBinding.instance.mouseTracker.sendMouseNotifications(<int>{0});
ui.window.onPointerDataPacket(packet2); ui.window.onPointerDataPacket(packet1);
tracker.collectMousePositions(); ui.window.onPointerDataPacket(packet2);
expect(enter.length, equals(1), reason: 'enter contains $enter'); expect(enter.length, equals(1), reason: 'enter contains $enter');
expect(enter.first.position, equals(const Offset(1.0, 101.0))); expect(enter.first.position, equals(const Offset(0.0, 0.0)));
expect(enter.first.delta, equals(const Offset(1.0, 101.0))); expect(enter.first.device, equals(0));
expect(enter.first.device, equals(0)); expect(enter.first.runtimeType, equals(PointerEnterEvent));
expect(enter.first.runtimeType, equals(PointerEnterEvent)); expect(move.length, equals(2), reason: 'move contains $move');
expect(move.length, equals(1), reason: 'move contains $move'); expect(move[0].position, equals(const Offset(0.0, 0.0)));
expect(move.first.position, equals(const Offset(1.0, 101.0))); expect(move[0].device, equals(0));
expect(move.first.delta, equals(const Offset(1.0, 101.0))); expect(move[0].runtimeType, equals(PointerHoverEvent));
expect(move.first.device, equals(0)); expect(move[1].position, equals(const Offset(1.0, 101.0)));
expect(move.first.runtimeType, equals(PointerHoverEvent)); expect(move[1].device, equals(0));
expect(exit.length, equals(1), reason: 'exit contains $exit'); expect(move[1].runtimeType, equals(PointerHoverEvent));
expect(exit.first.position, equals(const Offset(1.0, 201.0))); expect(exit.length, equals(0), reason: 'exit contains $exit');
expect(exit.first.delta, equals(const Offset(0.0, 0.0)));
expect(exit.first.device, equals(0));
expect(exit.first.runtimeType, equals(PointerExitEvent));
});
test('handles mouse down and move', () {
final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 0.0 * ui.window.devicePixelRatio,
physicalY: 0.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 101.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
]);
final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.down,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 101.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
ui.PointerData(
change: ui.PointerChange.move,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 201.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
]);
isInHitRegionOne = true;
tracker.attachAnnotation(annotation);
ui.window.onPointerDataPacket(packet1);
tracker.collectMousePositions();
ui.window.onPointerDataPacket(packet2);
tracker.collectMousePositions();
expect(enter.length, equals(1), reason: 'enter contains $enter');
expect(enter.first.position, equals(const Offset(1.0, 101.0)));
expect(enter.first.device, equals(0));
expect(enter.first.runtimeType, equals(PointerEnterEvent));
expect(move.length, equals(1), reason: 'move contains $move');
expect(move.first.position, equals(const Offset(1.0, 101.0)));
expect(move.first.device, equals(0));
expect(move.first.runtimeType, equals(PointerHoverEvent));
expect(exit.length, equals(0), reason: 'exit contains $exit');
});
}); });
} }
...@@ -388,7 +388,7 @@ void main() { ...@@ -388,7 +388,7 @@ void main() {
final TestGesture gesture = await tester.createGesture( final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse, kind: PointerDeviceKind.mouse,
); );
await gesture.addPointer(); await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await gesture.moveTo(center); await gesture.moveTo(center);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
......
...@@ -147,6 +147,7 @@ void main() { ...@@ -147,6 +147,7 @@ void main() {
TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(); await gesture.addPointer();
addTearDown(() => gesture?.removePointer()); addTearDown(() => gesture?.removePointer());
await tester.pumpAndSettle();
await gesture.moveTo(tester.getCenter(find.byType(FloatingActionButton))); await gesture.moveTo(tester.getCenter(find.byType(FloatingActionButton)));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
......
...@@ -8,7 +8,6 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -8,7 +8,6 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
// The tests in this file are moved from listener_test.dart, which tests several // The tests in this file are moved from listener_test.dart, which tests several
// deprecated APIs. The file should be removed once these parameters are. // deprecated APIs. The file should be removed once these parameters are.
...@@ -82,7 +81,7 @@ void main() { ...@@ -82,7 +81,7 @@ void main() {
// onPointer{Enter,Hover,Exit} are removed. They were kept for compatibility, // onPointer{Enter,Hover,Exit} are removed. They were kept for compatibility,
// and the tests have been copied to mouse_region_test. // and the tests have been copied to mouse_region_test.
// https://github.com/flutter/flutter/issues/36085 // https://github.com/flutter/flutter/issues/36085
setUp((){ setUp(() {
HoverClientState.numExits = 0; HoverClientState.numExits = 0;
HoverClientState.numEntries = 0; HoverClientState.numEntries = 0;
}); });
...@@ -91,19 +90,22 @@ void main() { ...@@ -91,19 +90,22 @@ void main() {
PointerEnterEvent enter; PointerEnterEvent enter;
PointerHoverEvent move; PointerHoverEvent move;
PointerExitEvent exit; PointerExitEvent exit;
await tester.pumpWidget(Center( await tester.pumpWidget(
child: Listener( Center(
child: Container( child: Listener(
color: const Color.fromARGB(0xff, 0xff, 0x00, 0x00), child: Container(
width: 100.0, color: const Color.fromARGB(0xff, 0xff, 0x00, 0x00),
height: 100.0, width: 100.0,
height: 100.0,
),
onPointerEnter: (PointerEnterEvent details) => enter = details,
onPointerHover: (PointerHoverEvent details) => move = details,
onPointerExit: (PointerExitEvent details) => exit = details,
), ),
onPointerEnter: (PointerEnterEvent details) => enter = details,
onPointerHover: (PointerHoverEvent details) => move = details,
onPointerExit: (PointerExitEvent details) => exit = details,
), ),
)); );
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
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();
...@@ -117,20 +119,22 @@ void main() { ...@@ -117,20 +119,22 @@ void main() {
PointerEnterEvent enter; PointerEnterEvent enter;
PointerHoverEvent move; PointerHoverEvent move;
PointerExitEvent exit; PointerExitEvent exit;
await tester.pumpWidget(Center( await tester.pumpWidget(
child: Listener( Center(
child: Container( child: Listener(
width: 100.0, child: Container(
height: 100.0, width: 100.0,
height: 100.0,
),
onPointerEnter: (PointerEnterEvent details) => enter = details,
onPointerHover: (PointerHoverEvent details) => move = details,
onPointerExit: (PointerExitEvent details) => exit = details,
), ),
onPointerEnter: (PointerEnterEvent details) => enter = details,
onPointerHover: (PointerHoverEvent details) => move = details,
onPointerExit: (PointerExitEvent details) => exit = details,
), ),
)); );
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: const Offset(400.0, 300.0));
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await gesture.moveTo(const Offset(400.0, 300.0));
await tester.pump(); await tester.pump();
move = null; move = null;
enter = null; enter = null;
...@@ -145,24 +149,25 @@ void main() { ...@@ -145,24 +149,25 @@ void main() {
PointerEnterEvent enter; PointerEnterEvent enter;
PointerHoverEvent move; PointerHoverEvent move;
PointerExitEvent exit; PointerExitEvent exit;
await tester.pumpWidget(Center( await tester.pumpWidget(
child: Listener( Center(
child: Container( child: Listener(
width: 100.0, child: Container(
height: 100.0, width: 100.0,
height: 100.0,
),
onPointerEnter: (PointerEnterEvent details) => enter = details,
onPointerHover: (PointerHoverEvent details) => move = details,
onPointerExit: (PointerExitEvent details) => exit = details,
), ),
onPointerEnter: (PointerEnterEvent details) => enter = details,
onPointerHover: (PointerHoverEvent details) => move = details,
onPointerExit: (PointerExitEvent details) => exit = details,
), ),
)); );
final RenderMouseRegion renderListener = tester.renderObject(find.byType(MouseRegion)); final RenderMouseRegion renderListener = tester.renderObject(find.byType(MouseRegion));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: const Offset(400.0, 300.0));
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await gesture.moveTo(const Offset(400.0, 300.0));
await tester.pump(); await tester.pump();
expect(move, isNotNull); expect(move, isNull);
expect(move.position, equals(const Offset(400.0, 300.0)));
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);
...@@ -197,7 +202,7 @@ void main() { ...@@ -197,7 +202,7 @@ void main() {
await tester.pumpWidget(Container()); await tester.pumpWidget(Container());
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await gesture.moveTo(const Offset(400.0, 0.0)); await gesture.addPointer(location: const Offset(400.0, 0.0));
await tester.pump(); await tester.pump();
await tester.pumpWidget( await tester.pumpWidget(
Column( Column(
...@@ -430,9 +435,8 @@ void main() { ...@@ -430,9 +435,8 @@ void main() {
expect(bottomLeft.dy - topLeft.dy, scaleFactor * localHeight); expect(bottomLeft.dy - topLeft.dy, scaleFactor * localHeight);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(); await gesture.addPointer(location: topLeft - const Offset(1, 1));
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await gesture.moveTo(topLeft - const Offset(1, 1));
await tester.pump(); await tester.pump();
expect(events, isEmpty); expect(events, isEmpty);
...@@ -458,7 +462,7 @@ void main() { ...@@ -458,7 +462,7 @@ void main() {
testWidgets('needsCompositing updates correctly and is respected', (WidgetTester tester) async { testWidgets('needsCompositing updates correctly and is respected', (WidgetTester tester) async {
// Pretend that we have a mouse connected. // Pretend that we have a mouse connected.
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(); await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await tester.pumpWidget( await tester.pumpWidget(
...@@ -507,7 +511,7 @@ void main() { ...@@ -507,7 +511,7 @@ void main() {
testWidgets("Callbacks aren't called during build", (WidgetTester tester) async { testWidgets("Callbacks aren't called during build", (WidgetTester tester) async {
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(); await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await tester.pumpWidget( await tester.pumpWidget(
...@@ -538,7 +542,7 @@ void main() { ...@@ -538,7 +542,7 @@ void main() {
testWidgets("Listener activate/deactivate don't duplicate annotations", (WidgetTester tester) async { testWidgets("Listener activate/deactivate don't duplicate annotations", (WidgetTester tester) async {
final GlobalKey feedbackKey = GlobalKey(); final GlobalKey feedbackKey = GlobalKey();
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(); await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await tester.pumpWidget( await tester.pumpWidget(
...@@ -586,11 +590,8 @@ void main() { ...@@ -586,11 +590,8 @@ void main() {
// Plug-in a mouse and move it to the center of the container. // Plug-in a mouse and move it to the center of the container.
TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(); await gesture.addPointer(location: Offset.zero);
addTearDown(() async { addTearDown(() => gesture?.removePointer());
if (gesture != null)
return gesture.removePointer();
});
await gesture.moveTo(tester.getCenter(find.byType(Container))); await gesture.moveTo(tester.getCenter(find.byType(Container)));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
...@@ -613,7 +614,7 @@ void main() { ...@@ -613,7 +614,7 @@ void main() {
expect(hover.length, 0); expect(hover.length, 0);
expect(exit.length, 1); expect(exit.length, 1);
expect(exit.single.position, const Offset(400.0, 300.0)); expect(exit.single.position, const Offset(400.0, 300.0));
expect(exit.single.delta, const Offset(0.0, 0.0)); expect(exit.single.delta, Offset.zero);
}); });
}); });
} }
...@@ -7,7 +7,6 @@ import 'package:flutter/rendering.dart'; ...@@ -7,7 +7,6 @@ import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
class HoverClient extends StatefulWidget { class HoverClient extends StatefulWidget {
const HoverClient({ const HoverClient({
Key key, Key key,
...@@ -27,18 +26,19 @@ class HoverClient extends StatefulWidget { ...@@ -27,18 +26,19 @@ class HoverClient extends StatefulWidget {
} }
class HoverClientState extends State<HoverClient> { class HoverClientState extends State<HoverClient> {
void _onExit(PointerExitEvent details) { void _onExit(PointerExitEvent details) {
if (widget.onExit != null) if (widget.onExit != null) {
widget.onExit(); widget.onExit();
}
if (widget.onHover != null) { if (widget.onHover != null) {
widget.onHover(false); widget.onHover(false);
} }
} }
void _onEnter(PointerEnterEvent details) { void _onEnter(PointerEnterEvent details) {
if (widget.onEnter != null) if (widget.onEnter != null) {
widget.onEnter(); widget.onEnter();
}
if (widget.onHover != null) { if (widget.onHover != null) {
widget.onHover(true); widget.onHover(true);
} }
...@@ -82,622 +82,681 @@ class _HoverFeedbackState extends State<HoverFeedback> { ...@@ -82,622 +82,681 @@ class _HoverFeedbackState extends State<HoverFeedback> {
} }
void main() { void main() {
group('MouseRegion hover detection', () { testWidgets('detects pointer enter', (WidgetTester tester) async {
testWidgets('detects pointer enter', (WidgetTester tester) async { PointerEnterEvent enter;
PointerEnterEvent enter; PointerHoverEvent move;
PointerHoverEvent move; PointerExitEvent exit;
PointerExitEvent exit; await tester.pumpWidget(Center(
await tester.pumpWidget(Center( child: MouseRegion(
child: MouseRegion( child: Container(
child: Container( color: const Color.fromARGB(0xff, 0xff, 0x00, 0x00),
color: const Color.fromARGB(0xff, 0xff, 0x00, 0x00), width: 100.0,
width: 100.0, height: 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);
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, isNotNull);
expect(enter.position, equals(const Offset(400.0, 300.0)));
expect(exit, isNull);
});
testWidgets('detects pointer exiting', (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,
), ),
)); onEnter: (PointerEnterEvent details) => enter = details,
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); onHover: (PointerHoverEvent details) => move = details,
addTearDown(gesture.removePointer); onExit: (PointerExitEvent details) => exit = details,
await gesture.moveTo(const Offset(400.0, 300.0)); ),
await tester.pump(); ));
move = null; final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
enter = null; await gesture.addPointer(location: Offset.zero);
await gesture.moveTo(const Offset(1.0, 1.0)); addTearDown(gesture.removePointer);
await tester.pump(); await gesture.moveTo(const Offset(400.0, 300.0));
expect(move, isNull); await tester.pump();
expect(enter, isNull); expect(move, isNotNull);
expect(exit, isNotNull); expect(move.position, equals(const Offset(400.0, 300.0)));
expect(exit.position, equals(const Offset(1.0, 1.0))); expect(enter, isNotNull);
}); expect(enter.position, equals(const Offset(400.0, 300.0)));
testWidgets('detects pointer exit when widget disappears', (WidgetTester tester) async { expect(exit, isNull);
PointerEnterEvent enter; });
PointerHoverEvent move;
PointerExitEvent exit; testWidgets('detects pointer exiting', (WidgetTester tester) async {
await tester.pumpWidget(Center( PointerEnterEvent enter;
child: MouseRegion( PointerHoverEvent move;
child: Container( PointerExitEvent exit;
width: 100.0, await tester.pumpWidget(Center(
height: 100.0, child: MouseRegion(
), child: Container(
onEnter: (PointerEnterEvent details) => enter = details, width: 100.0,
onHover: (PointerHoverEvent details) => move = details, height: 100.0,
onExit: (PointerExitEvent details) => exit = details,
), ),
)); onEnter: (PointerEnterEvent details) => enter = details,
final RenderMouseRegion renderListener = tester.renderObject(find.byType(MouseRegion)); onHover: (PointerHoverEvent details) => move = details,
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); onExit: (PointerExitEvent details) => exit = details,
addTearDown(gesture.removePointer); ),
await gesture.moveTo(const Offset(400.0, 300.0)); ));
await tester.pump(); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
expect(move, isNotNull); await gesture.addPointer(location: Offset.zero);
expect(move.position, equals(const Offset(400.0, 300.0))); addTearDown(gesture.removePointer);
expect(enter, isNotNull); await gesture.moveTo(const Offset(400.0, 300.0));
expect(enter.position, equals(const Offset(400.0, 300.0))); await tester.pump();
expect(exit, isNull); move = null;
await tester.pumpWidget(Center( enter = 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 {
PointerEnterEvent enter;
PointerHoverEvent move;
PointerExitEvent exit;
await tester.pumpWidget(Center(
child: MouseRegion(
child: Container( child: Container(
width: 100.0, width: 100.0,
height: 100.0, height: 100.0,
), ),
)); onEnter: (PointerEnterEvent details) => enter = details,
expect(exit, isNotNull); onHover: (PointerHoverEvent details) => move = details,
expect(exit.position, equals(const Offset(400.0, 300.0))); onExit: (PointerExitEvent details) => exit = details,
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener.hoverAnnotation), isFalse); ),
}); ));
testWidgets('Hover works with nested listeners', (WidgetTester tester) async { final RenderMouseRegion renderListener = tester.renderObject(find.byType(MouseRegion));
final UniqueKey key1 = UniqueKey(); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
final UniqueKey key2 = UniqueKey(); await gesture.addPointer(location: Offset.zero);
final List<PointerEnterEvent> enter1 = <PointerEnterEvent>[]; addTearDown(gesture.removePointer);
final List<PointerHoverEvent> move1 = <PointerHoverEvent>[]; await gesture.moveTo(const Offset(400.0, 300.0));
final List<PointerExitEvent> exit1 = <PointerExitEvent>[]; await tester.pump();
final List<PointerEnterEvent> enter2 = <PointerEnterEvent>[]; expect(move, isNotNull);
final List<PointerHoverEvent> move2 = <PointerHoverEvent>[]; expect(move.position, equals(const Offset(400.0, 300.0)));
final List<PointerExitEvent> exit2 = <PointerExitEvent>[]; expect(enter, isNotNull);
void clearLists() { expect(enter.position, equals(const Offset(400.0, 300.0)));
enter1.clear(); expect(exit, isNull);
move1.clear(); await tester.pumpWidget(Center(
exit1.clear(); child: Container(
enter2.clear(); width: 100.0,
move2.clear(); height: 100.0,
exit2.clear(); ),
} ));
expect(exit, isNotNull);
await tester.pumpWidget(Container()); expect(exit.position, equals(const Offset(400.0, 300.0)));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener.hoverAnnotation), isFalse);
addTearDown(gesture.removePointer); });
await gesture.moveTo(const Offset(400.0, 0.0));
await tester.pump(); testWidgets('Hover works with nested listeners', (WidgetTester tester) async {
await tester.pumpWidget( final UniqueKey key1 = UniqueKey();
Column( final UniqueKey key2 = UniqueKey();
mainAxisAlignment: MainAxisAlignment.center, final List<PointerEnterEvent> enter1 = <PointerEnterEvent>[];
crossAxisAlignment: CrossAxisAlignment.center, final List<PointerHoverEvent> move1 = <PointerHoverEvent>[];
children: <Widget>[ final List<PointerExitEvent> exit1 = <PointerExitEvent>[];
MouseRegion( final List<PointerEnterEvent> enter2 = <PointerEnterEvent>[];
onEnter: (PointerEnterEvent details) => enter1.add(details), final List<PointerHoverEvent> move2 = <PointerHoverEvent>[];
onHover: (PointerHoverEvent details) => move1.add(details), final List<PointerExitEvent> exit2 = <PointerExitEvent>[];
onExit: (PointerExitEvent details) => exit1.add(details), void clearLists() {
key: key1, enter1.clear();
child: Container( move1.clear();
width: 200, exit1.clear();
height: 200, enter2.clear();
padding: const EdgeInsets.all(50.0), move2.clear();
child: MouseRegion( exit2.clear();
key: key2, }
onEnter: (PointerEnterEvent details) => enter2.add(details),
onHover: (PointerHoverEvent details) => move2.add(details), await tester.pumpWidget(Container());
onExit: (PointerExitEvent details) => exit2.add(details), final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
child: Container(), addTearDown(gesture.removePointer);
), await gesture.moveTo(const Offset(400.0, 0.0));
await tester.pump();
await tester.pumpWidget(
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
MouseRegion(
onEnter: (PointerEnterEvent details) => enter1.add(details),
onHover: (PointerHoverEvent details) => move1.add(details),
onExit: (PointerExitEvent details) => exit1.add(details),
key: key1,
child: Container(
width: 200,
height: 200,
padding: const EdgeInsets.all(50.0),
child: MouseRegion(
key: key2,
onEnter: (PointerEnterEvent details) => enter2.add(details),
onHover: (PointerHoverEvent details) => move2.add(details),
onExit: (PointerExitEvent details) => exit2.add(details),
child: Container(),
), ),
), ),
], ),
), ],
); ),
final RenderMouseRegion renderListener1 = tester.renderObject(find.byKey(key1)); );
final RenderMouseRegion renderListener2 = tester.renderObject(find.byKey(key2)); final RenderMouseRegion renderListener1 = tester.renderObject(find.byKey(key1));
Offset center = tester.getCenter(find.byKey(key2)); final RenderMouseRegion renderListener2 = tester.renderObject(find.byKey(key2));
await gesture.moveTo(center); Offset center = tester.getCenter(find.byKey(key2));
await tester.pump(); await gesture.moveTo(center);
expect(move2, isNotEmpty); await tester.pump();
expect(enter2, isNotEmpty); expect(move2, isNotEmpty);
expect(exit2, isEmpty); expect(enter2, isNotEmpty);
expect(move1, isNotEmpty); expect(exit2, isEmpty);
expect(move1.last.position, equals(center)); expect(move1, isNotEmpty);
expect(enter1, isNotEmpty); expect(move1.last.position, equals(center));
expect(enter1.last.position, equals(center)); expect(enter1, isNotEmpty);
expect(exit1, isEmpty); expect(enter1.last.position, equals(center));
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue); expect(exit1, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue); expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
clearLists(); expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
clearLists();
// Now make sure that exiting the child only triggers the child exit, not
// the parent too. // Now make sure that exiting the child only triggers the child exit, not
center = center - const Offset(75.0, 0.0); // the parent too.
await gesture.moveTo(center); center = center - const Offset(75.0, 0.0);
await tester.pumpAndSettle(); await gesture.moveTo(center);
expect(move2, isEmpty); await tester.pumpAndSettle();
expect(enter2, isEmpty); expect(move2, isEmpty);
expect(exit2, isNotEmpty); expect(enter2, isEmpty);
expect(move1, isNotEmpty); expect(exit2, isNotEmpty);
expect(move1.last.position, equals(center)); expect(move1, isNotEmpty);
expect(enter1, isEmpty); expect(move1.last.position, equals(center));
expect(exit1, isEmpty); expect(enter1, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue); expect(exit1, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue); expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
clearLists(); expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
}); clearLists();
testWidgets('Hover transfers between two listeners', (WidgetTester tester) async { });
final UniqueKey key1 = UniqueKey();
final UniqueKey key2 = UniqueKey(); testWidgets('Hover transfers between two listeners', (WidgetTester tester) async {
final List<PointerEnterEvent> enter1 = <PointerEnterEvent>[]; final UniqueKey key1 = UniqueKey();
final List<PointerHoverEvent> move1 = <PointerHoverEvent>[]; final UniqueKey key2 = UniqueKey();
final List<PointerExitEvent> exit1 = <PointerExitEvent>[]; final List<PointerEnterEvent> enter1 = <PointerEnterEvent>[];
final List<PointerEnterEvent> enter2 = <PointerEnterEvent>[]; final List<PointerHoverEvent> move1 = <PointerHoverEvent>[];
final List<PointerHoverEvent> move2 = <PointerHoverEvent>[]; final List<PointerExitEvent> exit1 = <PointerExitEvent>[];
final List<PointerExitEvent> exit2 = <PointerExitEvent>[]; final List<PointerEnterEvent> enter2 = <PointerEnterEvent>[];
void clearLists() { final List<PointerHoverEvent> move2 = <PointerHoverEvent>[];
enter1.clear(); final List<PointerExitEvent> exit2 = <PointerExitEvent>[];
move1.clear(); void clearLists() {
exit1.clear(); enter1.clear();
enter2.clear(); move1.clear();
move2.clear(); exit1.clear();
exit2.clear(); enter2.clear();
} move2.clear();
exit2.clear();
await tester.pumpWidget(Container()); }
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer); await tester.pumpWidget(Container());
await gesture.moveTo(const Offset(400.0, 0.0)); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await tester.pump(); await gesture.moveTo(const Offset(400.0, 0.0));
await tester.pumpWidget( await tester.pump();
Column( await tester.pumpWidget(
mainAxisAlignment: MainAxisAlignment.center, Column(
crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ crossAxisAlignment: CrossAxisAlignment.center,
MouseRegion( children: <Widget>[
key: key1, MouseRegion(
child: Container( key: key1,
width: 100.0, child: Container(
height: 100.0, width: 100.0,
), height: 100.0,
onEnter: (PointerEnterEvent details) => enter1.add(details),
onHover: (PointerHoverEvent details) => move1.add(details),
onExit: (PointerExitEvent details) => exit1.add(details),
), ),
MouseRegion( onEnter: (PointerEnterEvent details) => enter1.add(details),
key: key2, onHover: (PointerHoverEvent details) => move1.add(details),
child: Container( onExit: (PointerExitEvent details) => exit1.add(details),
width: 100.0, ),
height: 100.0, MouseRegion(
), key: key2,
onEnter: (PointerEnterEvent details) => enter2.add(details), child: Container(
onHover: (PointerHoverEvent details) => move2.add(details), width: 100.0,
onExit: (PointerExitEvent details) => exit2.add(details), height: 100.0,
), ),
], onEnter: (PointerEnterEvent details) => enter2.add(details),
), onHover: (PointerHoverEvent details) => move2.add(details),
); onExit: (PointerExitEvent details) => exit2.add(details),
final RenderMouseRegion renderListener1 = tester.renderObject(find.byKey(key1)); ),
final RenderMouseRegion renderListener2 = tester.renderObject(find.byKey(key2)); ],
final Offset center1 = tester.getCenter(find.byKey(key1)); ),
final Offset center2 = tester.getCenter(find.byKey(key2)); );
await gesture.moveTo(center1); final RenderMouseRegion renderListener1 = tester.renderObject(find.byKey(key1));
await tester.pump(); final RenderMouseRegion renderListener2 = tester.renderObject(find.byKey(key2));
expect(move1, isNotEmpty); final Offset center1 = tester.getCenter(find.byKey(key1));
expect(move1.last.position, equals(center1)); final Offset center2 = tester.getCenter(find.byKey(key2));
expect(enter1, isNotEmpty); await gesture.moveTo(center1);
expect(enter1.last.position, equals(center1)); await tester.pump();
expect(exit1, isEmpty); expect(move1, isNotEmpty);
expect(move2, isEmpty); expect(move1.last.position, equals(center1));
expect(enter2, isEmpty); expect(enter1, isNotEmpty);
expect(exit2, isEmpty); expect(enter1.last.position, equals(center1));
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue); expect(exit1, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue); expect(move2, isEmpty);
clearLists(); expect(enter2, isEmpty);
await gesture.moveTo(center2); expect(exit2, isEmpty);
await tester.pump(); expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
expect(move1, isEmpty); expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
expect(enter1, isEmpty); clearLists();
expect(exit1, isNotEmpty); await gesture.moveTo(center2);
expect(exit1.last.position, equals(center2)); await tester.pump();
expect(move2, isNotEmpty); expect(move1, isEmpty);
expect(move2.last.position, equals(center2)); expect(enter1, isEmpty);
expect(enter2, isNotEmpty); expect(exit1, isNotEmpty);
expect(enter2.last.position, equals(center2)); expect(exit1.last.position, equals(center2));
expect(exit2, isEmpty); expect(move2, isNotEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue); expect(move2.last.position, equals(center2));
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue); expect(enter2, isNotEmpty);
clearLists(); expect(enter2.last.position, equals(center2));
await gesture.moveTo(const Offset(400.0, 450.0)); expect(exit2, isEmpty);
await tester.pump(); expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
expect(move1, isEmpty); expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
expect(enter1, isEmpty); clearLists();
expect(exit1, isEmpty); await gesture.moveTo(const Offset(400.0, 450.0));
expect(move2, isEmpty); await tester.pump();
expect(enter2, isEmpty); expect(move1, isEmpty);
expect(exit2, isNotEmpty); expect(enter1, isEmpty);
expect(exit2.last.position, equals(const Offset(400.0, 450.0))); expect(exit1, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue); expect(move2, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue); expect(enter2, isEmpty);
clearLists(); expect(exit2, isNotEmpty);
await tester.pumpWidget(Container()); expect(exit2.last.position, equals(const Offset(400.0, 450.0)));
expect(move1, isEmpty); expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
expect(enter1, isEmpty); expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
expect(exit1, isEmpty); clearLists();
expect(move2, isEmpty); await tester.pumpWidget(Container());
expect(enter2, isEmpty); expect(move1, isEmpty);
expect(exit2, isEmpty); expect(enter1, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isFalse); expect(exit1, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isFalse); expect(move2, isEmpty);
}); expect(enter2, isEmpty);
expect(exit2, isEmpty);
testWidgets('needsCompositing set when parent class needsCompositing is set', (WidgetTester tester) async { expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isFalse);
await tester.pumpWidget( expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isFalse);
MouseRegion( });
onEnter: (PointerEnterEvent _) {},
child: const Opacity(opacity: 0.5, child: Placeholder()), testWidgets('MouseRegion uses updated callbacks', (WidgetTester tester) async {
final List<String> logs = <String>[];
Widget hoverableContainer({
PointerEnterEventListener onEnter,
PointerHoverEventListener onHover,
PointerExitEventListener onExit,
}) {
return Container(
alignment: Alignment.topLeft,
child: MouseRegion(
child: Container(
color: const Color.fromARGB(0xff, 0xff, 0x00, 0x00),
width: 100.0,
height: 100.0,
),
onEnter: onEnter,
onHover: onHover,
onExit: onExit,
), ),
); );
}
RenderMouseRegion listener = tester.renderObject(find.byType(MouseRegion).first); await tester.pumpWidget(hoverableContainer(
expect(listener.needsCompositing, isTrue); onEnter: (PointerEnterEvent details) => logs.add('enter1'),
onHover: (PointerHoverEvent details) => logs.add('hover1'),
onExit: (PointerExitEvent details) => logs.add('exit1'),
));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
// Start outside, move inside, then move outside
await gesture.moveTo(const Offset(150.0, 150.0));
await tester.pump();
await gesture.moveTo(const Offset(50.0, 50.0));
await tester.pump();
await gesture.moveTo(const Offset(150.0, 150.0));
await tester.pump();
expect(logs, <String>['enter1', 'hover1', 'exit1']);
logs.clear();
// Same tests but with updated callbacks
await tester.pumpWidget(hoverableContainer(
onEnter: (PointerEnterEvent details) => logs.add('enter2'),
onHover: (PointerHoverEvent details) => logs.add('hover2'),
onExit: (PointerExitEvent details) => logs.add('exit2'),
));
await gesture.moveTo(const Offset(150.0, 150.0));
await tester.pump();
await gesture.moveTo(const Offset(50.0, 50.0));
await tester.pump();
await gesture.moveTo(const Offset(150.0, 150.0));
await tester.pump();
expect(logs, <String>['enter2', 'hover2', 'exit2']);
});
await tester.pumpWidget( testWidgets('needsCompositing set when parent class needsCompositing is set', (WidgetTester tester) async {
MouseRegion( await tester.pumpWidget(
onEnter: (PointerEnterEvent _) {}, MouseRegion(
child: const Placeholder(), onEnter: (PointerEnterEvent _) {},
), child: const Opacity(opacity: 0.5, child: Placeholder()),
); ),
);
listener = tester.renderObject(find.byType(MouseRegion).first); RenderMouseRegion listener = tester.renderObject(find.byType(MouseRegion).first);
expect(listener.needsCompositing, isFalse); expect(listener.needsCompositing, isTrue);
});
await tester.pumpWidget(
testWidgets('works with transform', (WidgetTester tester) async { MouseRegion(
// Regression test for https://github.com/flutter/flutter/issues/31986. onEnter: (PointerEnterEvent _) {},
final Key key = UniqueKey(); child: const Placeholder(),
const double scaleFactor = 2.0; ),
const double localWidth = 150.0; );
const double localHeight = 100.0;
final List<PointerEvent> events = <PointerEvent>[]; listener = tester.renderObject(find.byType(MouseRegion).first);
expect(listener.needsCompositing, isFalse);
await tester.pumpWidget( });
MaterialApp(
home: Center( testWidgets('works with transform', (WidgetTester tester) async {
child: Transform.scale( // Regression test for https://github.com/flutter/flutter/issues/31986.
scale: scaleFactor, final Key key = UniqueKey();
child: MouseRegion( const double scaleFactor = 2.0;
onEnter: (PointerEnterEvent event) { const double localWidth = 150.0;
events.add(event); const double localHeight = 100.0;
}, final List<PointerEvent> events = <PointerEvent>[];
onHover: (PointerHoverEvent event) {
events.add(event); await tester.pumpWidget(
}, MaterialApp(
onExit: (PointerExitEvent event) { home: Center(
events.add(event); child: Transform.scale(
}, scale: scaleFactor,
child: Container( child: MouseRegion(
key: key, onEnter: (PointerEnterEvent event) {
color: Colors.blue, events.add(event);
height: localHeight, },
width: localWidth, onHover: (PointerHoverEvent event) {
child: const Text('Hi'), events.add(event);
), },
onExit: (PointerExitEvent event) {
events.add(event);
},
child: Container(
key: key,
color: Colors.blue,
height: localHeight,
width: localWidth,
child: const Text('Hi'),
), ),
), ),
), ),
), ),
); ),
);
final Offset topLeft = tester.getTopLeft(find.byKey(key)); final Offset topLeft = tester.getTopLeft(find.byKey(key));
final Offset topRight = tester.getTopRight(find.byKey(key)); final Offset topRight = tester.getTopRight(find.byKey(key));
final Offset bottomLeft = tester.getBottomLeft(find.byKey(key)); final Offset bottomLeft = tester.getBottomLeft(find.byKey(key));
expect(topRight.dx - topLeft.dx, scaleFactor * localWidth); expect(topRight.dx - topLeft.dx, scaleFactor * localWidth);
expect(bottomLeft.dy - topLeft.dy, scaleFactor * localHeight); expect(bottomLeft.dy - topLeft.dy, scaleFactor * localHeight);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await gesture.addPointer(); await gesture.addPointer();
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await gesture.moveTo(topLeft - const Offset(1, 1)); await gesture.moveTo(topLeft - const Offset(1, 1));
await tester.pump(); await tester.pump();
expect(events, isEmpty); expect(events, isEmpty);
await gesture.moveTo(topLeft + const Offset(1, 1)); await gesture.moveTo(topLeft + const Offset(1, 1));
await tester.pump(); await tester.pump();
expect(events, hasLength(2)); expect(events, hasLength(2));
expect(events.first, isA<PointerEnterEvent>()); expect(events.first, isA<PointerEnterEvent>());
expect(events.last, isA<PointerHoverEvent>()); expect(events.last, isA<PointerHoverEvent>());
events.clear(); events.clear();
await gesture.moveTo(bottomLeft + const Offset(1, -1)); await gesture.moveTo(bottomLeft + const Offset(1, -1));
await tester.pump(); await tester.pump();
expect(events.single, isA<PointerHoverEvent>()); expect(events.single, isA<PointerHoverEvent>());
expect(events.single.delta, const Offset(0.0, scaleFactor * localHeight - 2)); expect(events.single.delta, const Offset(0.0, scaleFactor * localHeight - 2));
events.clear(); events.clear();
await gesture.moveTo(bottomLeft + const Offset(1, 1)); await gesture.moveTo(bottomLeft + const Offset(1, 1));
await tester.pump(); await tester.pump();
expect(events.single, isA<PointerExitEvent>()); expect(events.single, isA<PointerExitEvent>());
events.clear(); events.clear();
}); });
testWidgets('needsCompositing updates correctly and is respected', (WidgetTester tester) async { testWidgets('needsCompositing updates correctly and is respected', (WidgetTester tester) async {
// Pretend that we have a mouse connected. // Pretend that we have a mouse connected.
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await gesture.addPointer(); await gesture.addPointer();
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await tester.pumpWidget( await tester.pumpWidget(
Transform.scale( Transform.scale(
scale: 2.0, scale: 2.0,
child: const MouseRegion(), child: const MouseRegion(),
), ),
); );
final RenderMouseRegion listener = tester.renderObject(find.byType(MouseRegion)); final RenderMouseRegion listener = tester.renderObject(find.byType(MouseRegion));
expect(listener.needsCompositing, isFalse); expect(listener.needsCompositing, isFalse);
// No TransformLayer for `Transform.scale` is added because composting is // No TransformLayer for `Transform.scale` is added because composting is
// not required and therefore the transform is executed on the canvas // not required and therefore the transform is executed on the canvas
// directly. (One TransformLayer is always present for the root // directly. (One TransformLayer is always present for the root
// transform.) // transform.)
expect(tester.layers.whereType<TransformLayer>(), hasLength(1)); expect(tester.layers.whereType<TransformLayer>(), hasLength(1));
await tester.pumpWidget( await tester.pumpWidget(
Transform.scale( Transform.scale(
scale: 2.0, scale: 2.0,
child: MouseRegion( child: MouseRegion(
onHover: (PointerHoverEvent _) { }, onHover: (PointerHoverEvent _) {},
),
),
);
expect(listener.needsCompositing, isTrue);
// Compositing is required, therefore a dedicated TransformLayer for
// `Transform.scale` is added.
expect(tester.layers.whereType<TransformLayer>(), hasLength(2));
await tester.pumpWidget(
Transform.scale(
scale: 2.0,
child: const MouseRegion(
),
), ),
); ),
expect(listener.needsCompositing, isFalse); );
// TransformLayer for `Transform.scale` is removed again as transform is expect(listener.needsCompositing, isTrue);
// executed directly on the canvas. // Compositing is required, therefore a dedicated TransformLayer for
expect(tester.layers.whereType<TransformLayer>(), hasLength(1)); // `Transform.scale` is added.
}); expect(tester.layers.whereType<TransformLayer>(), hasLength(2));
testWidgets("Callbacks aren't called during build", (WidgetTester tester) async { await tester.pumpWidget(
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); Transform.scale(
addTearDown(gesture.removePointer); scale: 2.0,
await gesture.addPointer(); child: const MouseRegion(),
addTearDown(gesture.removePointer); ),
);
int numEntries = 0; expect(listener.needsCompositing, isFalse);
int numExits = 0; // TransformLayer for `Transform.scale` is removed again as transform is
// executed directly on the canvas.
await tester.pumpWidget( expect(tester.layers.whereType<TransformLayer>(), hasLength(1));
Center(child: HoverFeedback( });
onEnter: () => numEntries++,
onExit: () => numExits++,
)),
);
await gesture.moveTo(tester.getCenter(find.byType(Text))); testWidgets("Callbacks aren't called during build", (WidgetTester tester) async {
await tester.pumpAndSettle(); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
expect(numEntries, equals(1)); addTearDown(gesture.removePointer);
expect(numExits, equals(0)); await gesture.addPointer(location: Offset.zero);
expect(find.text('HOVERING'), findsOneWidget);
await tester.pumpWidget( int numEntries = 0;
Container(), int numExits = 0;
);
await tester.pump();
expect(numEntries, equals(1));
expect(numExits, equals(1));
await tester.pumpWidget(
Center(child: HoverFeedback(
onEnter: () => numEntries++,
onExit: () => numExits++,
)),
);
await tester.pump();
expect(numEntries, equals(2));
expect(numExits, equals(1));
});
testWidgets("MouseRegion activate/deactivate don't duplicate annotations", (WidgetTester tester) async {
final GlobalKey feedbackKey = GlobalKey();
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await gesture.addPointer();
addTearDown(gesture.removePointer);
int numEntries = 0;
int numExits = 0;
await tester.pumpWidget(
Center(child: HoverFeedback(
key: feedbackKey,
onEnter: () => numEntries++,
onExit: () => numExits++,
)),
);
await gesture.moveTo(tester.getCenter(find.byType(Text))); await tester.pumpWidget(
await tester.pumpAndSettle(); Center(
expect(numEntries, equals(1)); child: HoverFeedback(
expect(numExits, equals(0)); onEnter: () => numEntries++,
expect(find.text('HOVERING'), findsOneWidget); onExit: () => numExits++,
)),
await tester.pumpWidget( );
Center(child: Container(child: HoverFeedback(
key: feedbackKey, await gesture.moveTo(tester.getCenter(find.byType(Text)));
onEnter: () => numEntries++, await tester.pumpAndSettle();
onExit: () => numExits++, expect(numEntries, equals(1));
))), expect(numExits, equals(0));
); expect(find.text('HOVERING'), findsOneWidget);
await tester.pump();
expect(numEntries, equals(2)); await tester.pumpWidget(
expect(numExits, equals(1)); Container(),
await tester.pumpWidget( );
Container(), await tester.pump();
); expect(numEntries, equals(1));
await tester.pump(); expect(numExits, equals(1));
expect(numEntries, equals(2));
expect(numExits, equals(2)); await tester.pumpWidget(
}); Center(
child: HoverFeedback(
testWidgets('Exit event when unplugging mouse should have a position', (WidgetTester tester) async { onEnter: () => numEntries++,
final List<PointerEnterEvent> enter = <PointerEnterEvent>[]; onExit: () => numExits++,
final List<PointerHoverEvent> hover = <PointerHoverEvent>[]; )),
final List<PointerExitEvent> exit = <PointerExitEvent>[]; );
await tester.pump();
await tester.pumpWidget( expect(numEntries, equals(2));
Center( expect(numExits, equals(1));
child: MouseRegion( });
onEnter: (PointerEnterEvent e) => enter.add(e),
onHover: (PointerHoverEvent e) => hover.add(e), testWidgets("MouseRegion activate/deactivate don't duplicate annotations", (WidgetTester tester) async {
onExit: (PointerExitEvent e) => exit.add(e), final GlobalKey feedbackKey = GlobalKey();
child: Container( final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
height: 100.0, addTearDown(gesture.removePointer);
width: 100.0, await gesture.addPointer();
), addTearDown(gesture.removePointer);
int numEntries = 0;
int numExits = 0;
await tester.pumpWidget(
Center(
child: HoverFeedback(
key: feedbackKey,
onEnter: () => numEntries++,
onExit: () => numExits++,
)),
);
await gesture.moveTo(tester.getCenter(find.byType(Text)));
await tester.pumpAndSettle();
expect(numEntries, equals(1));
expect(numExits, equals(0));
expect(find.text('HOVERING'), findsOneWidget);
await tester.pumpWidget(
Center(
child: Container(
child: HoverFeedback(
key: feedbackKey,
onEnter: () => numEntries++,
onExit: () => numExits++,
))),
);
await tester.pump();
expect(numEntries, equals(2));
expect(numExits, equals(1));
await tester.pumpWidget(
Container(),
);
await tester.pump();
expect(numEntries, equals(2));
expect(numExits, equals(2));
});
testWidgets('Exit event when unplugging mouse should have a position', (WidgetTester tester) async {
final List<PointerEnterEvent> enter = <PointerEnterEvent>[];
final List<PointerHoverEvent> hover = <PointerHoverEvent>[];
final List<PointerExitEvent> exit = <PointerExitEvent>[];
await tester.pumpWidget(
Center(
child: MouseRegion(
onEnter: (PointerEnterEvent e) => enter.add(e),
onHover: (PointerHoverEvent e) => hover.add(e),
onExit: (PointerExitEvent e) => exit.add(e),
child: Container(
height: 100.0,
width: 100.0,
), ),
), ),
); ),
);
// Plug-in a mouse and move it to the center of the container. // Plug-in a mouse and move it to the center of the container.
TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(); await gesture.addPointer(location: Offset.zero);
addTearDown(() => gesture?.removePointer()); addTearDown(() => gesture?.removePointer());
await gesture.moveTo(tester.getCenter(find.byType(Container))); await gesture.moveTo(tester.getCenter(find.byType(Container)));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
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));
expect(hover.length, 1); expect(hover.length, 1);
expect(hover.single.position, const Offset(400.0, 300.0)); expect(hover.single.position, const Offset(400.0, 300.0));
expect(exit.length, 0); expect(exit.length, 0);
enter.clear(); enter.clear();
hover.clear(); hover.clear();
exit.clear(); exit.clear();
// Unplug the mouse. // Unplug the mouse.
await gesture.removePointer(); await gesture.removePointer();
gesture = null; gesture = null;
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(enter.length, 0); expect(enter.length, 0);
expect(hover.length, 0); expect(hover.length, 0);
expect(exit.length, 1); expect(exit.length, 1);
expect(exit.single.position, const Offset(400.0, 300.0)); expect(exit.single.position, const Offset(400.0, 300.0));
expect(exit.single.delta, const Offset(0.0, 0.0)); expect(exit.single.delta, Offset.zero);
});
testWidgets('detects pointer enter with closure arguments', (WidgetTester tester) async {
await tester.pumpWidget(_HoverClientWithClosures());
expect(find.text('not hovering'), findsOneWidget);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await gesture.addPointer();
// Move to a position out of MouseRegion
await gesture.moveTo(tester.getBottomRight(find.byType(MouseRegion)) + const Offset(10, -10));
await tester.pumpAndSettle();
expect(find.text('not hovering'), findsOneWidget);
// Move into MouseRegion
await gesture.moveBy(const Offset(-20, 0));
await tester.pumpAndSettle();
expect(find.text('HOVERING'), findsOneWidget);
});
}); });
group('MouseRegion paints child once and only once', () { testWidgets('detects pointer enter with closure arguments', (WidgetTester tester) async {
testWidgets('When MouseRegion is inactive', (WidgetTester tester) async { await tester.pumpWidget(_HoverClientWithClosures());
int paintCount = 0; expect(find.text('not hovering'), findsOneWidget);
await tester.pumpWidget(
Directionality( final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
textDirection: TextDirection.ltr, addTearDown(gesture.removePointer);
child: MouseRegion( await gesture.addPointer();
onEnter: (PointerEnterEvent e) {}, // Move to a position out of MouseRegion
child: _PaintDelegateWidget( await gesture.moveTo(tester.getBottomRight(find.byType(MouseRegion)) + const Offset(10, -10));
onPaint: _VoidDelegate(() => paintCount++), await tester.pumpAndSettle();
child: const Text('123'), expect(find.text('not hovering'), findsOneWidget);
),
// Move into MouseRegion
await gesture.moveBy(const Offset(-20, 0));
await tester.pumpAndSettle();
expect(find.text('HOVERING'), findsOneWidget);
});
testWidgets('MouseRegion paints child once and only once when MouseRegion is inactive', (WidgetTester tester) async {
int paintCount = 0;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
onEnter: (PointerEnterEvent e) {},
child: _PaintDelegateWidget(
onPaint: _VoidDelegate(() => paintCount++),
child: const Text('123'),
), ),
), ),
); ),
);
expect(paintCount, 1); expect(paintCount, 1);
}); });
testWidgets('When MouseRegion is active', (WidgetTester tester) async { testWidgets('MouseRegion paints child once and only once when MouseRegion is active', (WidgetTester tester) async {
int paintCount = 0; int paintCount = 0;
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(); await gesture.addPointer();
addTearDown(gesture.removePointer);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: MouseRegion( child: MouseRegion(
onEnter: (PointerEnterEvent e) {}, onEnter: (PointerEnterEvent e) {},
child: _PaintDelegateWidget( child: _PaintDelegateWidget(
onPaint: _VoidDelegate(() => paintCount++), onPaint: _VoidDelegate(() => paintCount++),
child: const Text('123'), child: const Text('123'),
),
), ),
), ),
); ),
);
expect(paintCount, 1); expect(paintCount, 1);
await gesture.removePointer();
});
}); });
testWidgets('RenderMouseRegion\'s debugFillProperties when default', (WidgetTester tester) async { testWidgets('RenderMouseRegion\'s debugFillProperties when default', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
RenderMouseRegion().debugFillProperties(builder); RenderMouseRegion().debugFillProperties(builder);
final List<String> description = builder.properties final List<String> description = builder.properties.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)).map((DiagnosticsNode node) => node.toString()).toList();
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description, <String>[ expect(description, <String>[
'parentData: MISSING', 'parentData: MISSING',
...@@ -716,10 +775,7 @@ void main() { ...@@ -716,10 +775,7 @@ void main() {
child: RenderErrorBox(), child: RenderErrorBox(),
).debugFillProperties(builder); ).debugFillProperties(builder);
final List<String> description = builder.properties final List<String> description = builder.properties.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)).map((DiagnosticsNode node) => node.toString()).toList();
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description, <String>[ expect(description, <String>[
'parentData: MISSING', 'parentData: MISSING',
...@@ -728,6 +784,39 @@ void main() { ...@@ -728,6 +784,39 @@ void main() {
'listeners: enter, hover, exit', 'listeners: enter, hover, exit',
]); ]);
}); });
testWidgets('No new frames are scheduled when mouse moves without triggering callbacks', (WidgetTester tester) async {
await tester.pumpWidget(Center(
child: MouseRegion(
child: Container(
width: 100.0,
height: 100.0,
),
onEnter: (PointerEnterEvent details) {},
onHover: (PointerHoverEvent details) {},
onExit: (PointerExitEvent details) {},
),
));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: const Offset(400.0, 300.0));
addTearDown(gesture.removePointer);
await tester.pumpAndSettle();
await gesture.moveBy(const Offset(10.0, 10.0));
expect(tester.binding.hasScheduledFrame, isFalse);
});
testWidgets("MouseTracker's attachAnnotation doesn't schedule any frames", (WidgetTester tester) async {
// This test is here because MouseTracker can't use testWidgets.
final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
onEnter: (PointerEnterEvent event) {},
onHover: (PointerHoverEvent event) {},
onExit: (PointerExitEvent event) {},
);
RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
expect(tester.binding.hasScheduledFrame, isFalse);
expect(RendererBinding.instance.mouseTracker.isAnnotationAttached(annotation), isTrue);
RendererBinding.instance.mouseTracker.detachAnnotation(annotation);
});
} }
// This widget allows you to send a callback that is called during `onPaint. // This widget allows you to send a callback that is called during `onPaint.
...@@ -748,8 +837,7 @@ class _PaintDelegateWidget extends SingleChildRenderObjectWidget { ...@@ -748,8 +837,7 @@ class _PaintDelegateWidget extends SingleChildRenderObjectWidget {
@override @override
void updateRenderObject(BuildContext context, _PaintCallbackObject renderObject) { void updateRenderObject(BuildContext context, _PaintCallbackObject renderObject) {
renderObject renderObject..onPaint = onPaint?.callback;
..onPaint = onPaint?.callback;
} }
} }
...@@ -769,8 +857,9 @@ class _PaintCallbackObject extends RenderProxyBox { ...@@ -769,8 +857,9 @@ class _PaintCallbackObject extends RenderProxyBox {
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (onPaint != null) if (onPaint != null) {
onPaint(); onPaint();
}
super.paint(context, offset); super.paint(context, offset);
} }
} }
...@@ -781,7 +870,6 @@ class _HoverClientWithClosures extends StatefulWidget { ...@@ -781,7 +870,6 @@ class _HoverClientWithClosures extends StatefulWidget {
} }
class _HoverClientWithClosuresState extends State<_HoverClientWithClosures> { class _HoverClientWithClosuresState extends State<_HoverClientWithClosures> {
bool _hovering = false; bool _hovering = false;
@override @override
...@@ -789,8 +877,16 @@ class _HoverClientWithClosuresState extends State<_HoverClientWithClosures> { ...@@ -789,8 +877,16 @@ class _HoverClientWithClosuresState extends State<_HoverClientWithClosures> {
return Directionality( return Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: MouseRegion( child: MouseRegion(
onEnter: (PointerEnterEvent _) { setState(() { _hovering = true; }); }, onEnter: (PointerEnterEvent _) {
onExit: (PointerExitEvent _) { setState(() { _hovering = false; }); }, setState(() {
_hovering = true;
});
},
onExit: (PointerExitEvent _) {
setState(() {
_hovering = false;
});
},
child: Text(_hovering ? 'HOVERING' : 'not hovering'), child: Text(_hovering ? 'HOVERING' : 'not hovering'),
), ),
); );
......
...@@ -805,6 +805,10 @@ abstract class TestWidgetsFlutterBinding extends BindingBase ...@@ -805,6 +805,10 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
'The MouseTracker thinks that there is still a mouse connected, which indicates that a ' 'The MouseTracker thinks that there is still a mouse connected, which indicates that a '
'test has not removed the mouse pointer which it added. Call removePointer on the ' 'test has not removed the mouse pointer which it added. Call removePointer on the '
'active mouse gesture to remove the mouse pointer.'); 'active mouse gesture to remove the mouse pointer.');
// ignore: invalid_use_of_visible_for_testing_member
RendererBinding.instance.initMouseTracker();
// ignore: invalid_use_of_visible_for_testing_member
PointerEventConverter.clearPointers();
} }
} }
......
...@@ -373,16 +373,16 @@ class TestGesture { ...@@ -373,16 +373,16 @@ class TestGesture {
} }
/// In a test, send a pointer add event for this pointer. /// In a test, send a pointer add event for this pointer.
Future<void> addPointer({ Duration timeStamp = Duration.zero }) { Future<void> addPointer({ Duration timeStamp = Duration.zero, Offset location }) {
return TestAsyncUtils.guard<void>(() { return TestAsyncUtils.guard<void>(() {
return _dispatcher(_pointer.addPointer(timeStamp: timeStamp, location: _pointer.location), null); return _dispatcher(_pointer.addPointer(timeStamp: timeStamp, location: location ?? _pointer.location), null);
}); });
} }
/// In a test, send a pointer remove event for this pointer. /// In a test, send a pointer remove event for this pointer.
Future<void> removePointer({ Duration timeStamp = Duration.zero}) { Future<void> removePointer({ Duration timeStamp = Duration.zero, Offset location }) {
return TestAsyncUtils.guard<void>(() { return TestAsyncUtils.guard<void>(() {
return _dispatcher(_pointer.removePointer(timeStamp: timeStamp, location: _pointer.location), null); return _dispatcher(_pointer.removePointer(timeStamp: timeStamp, location: location ?? _pointer.location), null);
}); });
} }
......
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