diff --git a/packages/flutter/lib/src/gestures/mouse_tracking.dart b/packages/flutter/lib/src/gestures/mouse_tracking.dart index 7f6d30f053..77b35e508a 100644 --- a/packages/flutter/lib/src/gestures/mouse_tracking.dart +++ b/packages/flutter/lib/src/gestures/mouse_tracking.dart @@ -78,7 +78,7 @@ class _TrackedAnnotation { /// /// It is used by the [MouseTracker] to fetch annotations for the mouse /// position. -typedef MouseDetectorAnnotationFinder = MouseTrackerAnnotation Function(Offset offset); +typedef MouseDetectorAnnotationFinder = Iterable Function(Offset offset); /// Keeps state about which objects are interested in tracking mouse positions /// and notifies them when a mouse pointer enters, moves, or leaves an annotated @@ -229,42 +229,44 @@ class MouseTracker extends ChangeNotifier { for (int deviceId in _lastMouseEvent.keys) { final PointerEvent lastEvent = _lastMouseEvent[deviceId]; - final MouseTrackerAnnotation hit = annotationFinder(lastEvent.position); + final Iterable hits = annotationFinder(lastEvent.position); - // No annotation was found at this position for this deviceId, so send an + // No annotations were found at this position for this deviceId, so send an // exit to all active tracked annotations, since none of them were hit. - if (hit == null) { + if (hits.isEmpty) { // Send an exit to all tracked animations tracking this deviceId. for (_TrackedAnnotation trackedAnnotation in _trackedAnnotations.values) { exitAnnotation(trackedAnnotation, deviceId); } - return; + continue; } - final _TrackedAnnotation hitAnnotation = _findAnnotation(hit); - if (!hitAnnotation.activeDevices.contains(deviceId)) { - // A tracked annotation that just became active and needs to have an enter - // event sent to it. - hitAnnotation.activeDevices.add(deviceId); - if (hitAnnotation.annotation?.onEnter != null) { - hitAnnotation.annotation.onEnter(PointerEnterEvent.fromMouseEvent(lastEvent)); - } - } - if (hitAnnotation.annotation?.onHover != null && lastEvent is PointerHoverEvent) { - hitAnnotation.annotation.onHover(lastEvent); - } - - // Tell any tracked annotations that weren't hit that they are no longer - // active. - for (_TrackedAnnotation trackedAnnotation in _trackedAnnotations.values) { - if (hitAnnotation == trackedAnnotation) { - continue; - } - if (trackedAnnotation.activeDevices.contains(deviceId)) { - if (trackedAnnotation.annotation?.onExit != null) { - trackedAnnotation.annotation.onExit(PointerExitEvent.fromMouseEvent(lastEvent)); + final Set<_TrackedAnnotation> hitAnnotations = hits.map<_TrackedAnnotation>((MouseTrackerAnnotation hit) => _findAnnotation(hit)).toSet(); + for (_TrackedAnnotation hitAnnotation in hitAnnotations) { + if (!hitAnnotation.activeDevices.contains(deviceId)) { + // A tracked annotation that just became active and needs to have an enter + // event sent to it. + hitAnnotation.activeDevices.add(deviceId); + if (hitAnnotation.annotation?.onEnter != null) { + hitAnnotation.annotation.onEnter(PointerEnterEvent.fromMouseEvent(lastEvent)); + } + } + if (hitAnnotation.annotation?.onHover != null && lastEvent is PointerHoverEvent) { + hitAnnotation.annotation.onHover(lastEvent); + } + + // Tell any tracked annotations that weren't hit that they are no longer + // active. + for (_TrackedAnnotation trackedAnnotation in _trackedAnnotations.values) { + if (hitAnnotations.contains(trackedAnnotation)) { + continue; + } + if (trackedAnnotation.activeDevices.contains(deviceId)) { + if (trackedAnnotation.annotation?.onExit != null) { + trackedAnnotation.annotation.onExit(PointerExitEvent.fromMouseEvent(lastEvent)); + } + trackedAnnotation.activeDevices.remove(deviceId); } - trackedAnnotation.activeDevices.remove(deviceId); } } } diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart index 261b5669fb..03448328b2 100644 --- a/packages/flutter/lib/src/rendering/binding.dart +++ b/packages/flutter/lib/src/rendering/binding.dart @@ -246,8 +246,7 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture // Layer hit testing is done using device pixels, so we have to convert // the logical coordinates of the event location back to device pixels // here. - return renderView.layer - .find(offset * window.devicePixelRatio); + return renderView.layer.findAll(offset * window.devicePixelRatio); }); } diff --git a/packages/flutter/lib/src/rendering/layer.dart b/packages/flutter/lib/src/rendering/layer.dart index 9970900eb0..38a942d0fd 100644 --- a/packages/flutter/lib/src/rendering/layer.dart +++ b/packages/flutter/lib/src/rendering/layer.dart @@ -163,7 +163,7 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin { /// /// Returns null if no matching region is found. /// - /// The main way for a value to be assigned here is by pushing an + /// The main way for a value to be found here is by pushing an /// [AnnotatedRegionLayer] into the layer tree. /// /// See also: @@ -171,6 +171,19 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin { /// * [AnnotatedRegionLayer], for placing values in the layer tree. S find(Offset regionOffset); + /// Returns an iterable of [S] values that corresponds to the point described + /// by [regionOffset] on all layers under the point. + /// + /// Returns an empty list if no matching region is found. + /// + /// The main way for a value to be found here is by pushing an + /// [AnnotatedRegionLayer] into the layer tree. + /// + /// See also: + /// + /// * [AnnotatedRegionLayer], for placing values in the layer tree. + Iterable findAll(Offset regionOffset); + /// Override this method to upload this layer to the engine. /// /// Return the engine layer for retained rendering. When there's no @@ -290,6 +303,9 @@ class PictureLayer extends Layer { @override S find(Offset regionOffset) => null; + + @override + Iterable findAll(Offset regionOffset) => []; } /// A composited layer that maps a backend texture to a rectangle. @@ -359,6 +375,9 @@ class TextureLayer extends Layer { @override S find(Offset regionOffset) => null; + + @override + Iterable findAll(Offset regionOffset) => []; } /// A layer that shows an embedded [UIView](https://developer.apple.com/documentation/uikit/uiview) @@ -395,6 +414,9 @@ class PlatformViewLayer extends Layer { @override S find(Offset regionOffset) => null; + + @override + Iterable findAll(Offset regionOffset) => []; } /// A layer that indicates to the compositor that it should display @@ -468,6 +490,9 @@ class PerformanceOverlayLayer extends Layer { @override S find(Offset regionOffset) => null; + + @override + Iterable findAll(Offset regionOffset) => []; } /// A composited layer that has a list of children. @@ -609,15 +634,24 @@ class ContainerLayer extends Layer { @override S find(Offset regionOffset) { - Layer current = lastChild; - while (current != null) { - final Object value = current.find(regionOffset); - if (value != null) { - return value; - } - current = current.previousSibling; + final Iterable all = findAll(regionOffset); + if (all.isEmpty) { + return null; + } + return all.first; + } + + @override + Iterable findAll(Offset regionOffset) sync* { + if (firstChild == null) + return; + Layer child = lastChild; + while (true) { + yield* child.findAll(regionOffset); + if (child == firstChild) + break; + child = child.previousSibling; } - return null; } @override @@ -844,6 +878,11 @@ class OffsetLayer extends ContainerLayer { return super.find(regionOffset - offset); } + @override + Iterable findAll(Offset regionOffset) { + return super.findAll(regionOffset - offset); + } + @override void applyTransform(Layer child, Matrix4 transform) { assert(child != null); @@ -993,6 +1032,13 @@ class ClipRectLayer extends ContainerLayer { return super.find(regionOffset); } + @override + Iterable findAll(Offset regionOffset) sync* { + if (!clipRect.contains(regionOffset)) + return; + yield* super.findAll(regionOffset); + } + @override ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { bool enabled = true; @@ -1065,6 +1111,13 @@ class ClipRRectLayer extends ContainerLayer { return super.find(regionOffset); } + @override + Iterable findAll(Offset regionOffset) sync* { + if (!clipRRect.contains(regionOffset)) + return; + yield* super.findAll(regionOffset); + } + @override ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { bool enabled = true; @@ -1137,6 +1190,13 @@ class ClipPathLayer extends ContainerLayer { return super.find(regionOffset); } + @override + Iterable findAll(Offset regionOffset) sync* { + if (!clipPath.contains(regionOffset)) + return; + yield* super.findAll(regionOffset); + } + @override ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { bool enabled = true; @@ -1205,15 +1265,24 @@ class TransformLayer extends OffsetLayer { @override S find(Offset regionOffset) { + final Iterable all = findAll(regionOffset); + if (all.isEmpty) { + return null; + } + return all.first; + } + + @override + Iterable findAll(Offset regionOffset) sync* { if (_inverseDirty) { _invertedTransform = Matrix4.tryInvert(transform); _inverseDirty = false; } if (_invertedTransform == null) - return null; + return; final Vector4 vector = Vector4(regionOffset.dx, regionOffset.dy, 0.0, 1.0); final Vector4 result = _invertedTransform.transform(vector); - return super.find(Offset(result[0], result[1])); + yield* super.findAll(Offset(result[0], result[1])); } @override @@ -1519,9 +1588,18 @@ class PhysicalModelLayer extends ContainerLayer { @override S find(Offset regionOffset) { - if (!clipPath.contains(regionOffset)) + final Iterable all = findAll(regionOffset); + if (all.isEmpty) { return null; - return super.find(regionOffset); + } + return all.first; + } + + @override + Iterable findAll(Offset regionOffset) sync* { + if (!clipPath.contains(regionOffset)) + return; + yield* super.findAll(regionOffset); } @override @@ -1635,9 +1713,10 @@ class LeaderLayer extends ContainerLayer { Offset _lastOffset; @override - S find(Offset regionOffset) { - return super.find(regionOffset - offset); - } + S find(Offset regionOffset) => super.find(regionOffset - offset); + + @override + Iterable findAll(Offset regionOffset) => super.findAll(regionOffset - offset); @override ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { @@ -1751,11 +1830,7 @@ class FollowerLayer extends ContainerLayer { Matrix4 _invertedTransform; bool _inverseDirty = true; - @override - S find(Offset regionOffset) { - if (link.leader == null) { - return showWhenUnlinked ? super.find(regionOffset - unlinkedOffset) : null; - } + Offset _transformOffset(Offset regionOffset) { if (_inverseDirty) { _invertedTransform = Matrix4.tryInvert(getLastTransform()); _inverseDirty = false; @@ -1764,7 +1839,24 @@ class FollowerLayer extends ContainerLayer { return null; final Vector4 vector = Vector4(regionOffset.dx, regionOffset.dy, 0.0, 1.0); final Vector4 result = _invertedTransform.transform(vector); - return super.find(Offset(result[0] - linkedOffset.dx, result[1] - linkedOffset.dy)); + return Offset(result[0] - linkedOffset.dx, result[1] - linkedOffset.dy); + } + + @override + S find(Offset regionOffset) { + final Iterable all = findAll(regionOffset); + if (all.isEmpty) { + return null; + } + return all.first; + } + + @override + Iterable findAll(Offset regionOffset) { + if (link.leader == null) { + return showWhenUnlinked ? super.findAll(regionOffset - unlinkedOffset) : []; + } + return super.findAll(_transformOffset(regionOffset)); } /// The transform that was used during the last composition phase. @@ -1939,17 +2031,24 @@ class AnnotatedRegionLayer extends ContainerLayer { @override S find(Offset regionOffset) { - final S result = super.find(regionOffset); - if (result != null) - return result; - if (size != null && !(offset & size).contains(regionOffset)) + final Iterable all = findAll(regionOffset); + if (all.isEmpty) { return null; + } + return all.first; + } + + @override + Iterable findAll(Offset regionOffset) sync* { + yield* super.findAll(regionOffset); + if (size != null && !(offset & size).contains(regionOffset)) { + return; + } if (T == S) { final Object untypedResult = value; final S typedResult = untypedResult; - return typedResult; + yield typedResult; } - return super.find(regionOffset); } @override diff --git a/packages/flutter/lib/src/widgets/widget_inspector.dart b/packages/flutter/lib/src/widgets/widget_inspector.dart index 0c03a449b9..032d993965 100644 --- a/packages/flutter/lib/src/widgets/widget_inspector.dart +++ b/packages/flutter/lib/src/widgets/widget_inspector.dart @@ -60,6 +60,9 @@ class _ProxyLayer extends Layer { @override S find(Offset regionOffset) => _layer.find(regionOffset); + + @override + Iterable findAll(Offset regionOffset) => []; } /// A [Canvas] that multicasts all method calls to a main canvas and a @@ -2799,6 +2802,9 @@ class _InspectorOverlayLayer extends Layer { @override S find(Offset regionOffset) => null; + + @override + Iterable findAll(Offset regionOffset) => []; } const double _kScreenEdgeMargin = 10.0; diff --git a/packages/flutter/test/gestures/mouse_tracking_test.dart b/packages/flutter/test/gestures/mouse_tracking_test.dart index c2483ac7e2..687ad978a1 100644 --- a/packages/flutter/test/gestures/mouse_tracking_test.dart +++ b/packages/flutter/test/gestures/mouse_tracking_test.dart @@ -65,12 +65,11 @@ void main() { isInHitRegionTwo = false; tracker = MouseTracker( GestureBinding.instance.pointerRouter, - (Offset _) { + (Offset _) sync* { if (isInHitRegionOne) - return annotation; + yield annotation; else if (isInHitRegionTwo) - return partialAnnotation; - return null; + yield partialAnnotation; }, ); }); diff --git a/packages/flutter/test/rendering/annotated_region_test.dart b/packages/flutter/test/rendering/annotated_region_test.dart index 4c71e78e98..204eee69ad 100644 --- a/packages/flutter/test/rendering/annotated_region_test.dart +++ b/packages/flutter/test/rendering/annotated_region_test.dart @@ -7,7 +7,7 @@ import 'package:flutter/widgets.dart'; import '../flutter_test_alternative.dart'; void main() { - group(AnnotatedRegion, () { + group('$AnnotatedRegion find', () { test('finds the first value in a OffsetLayer when sized', () { final ContainerLayer containerLayer = ContainerLayer(); final List layers = [ @@ -136,4 +136,154 @@ void main() { expect(parent.find(const Offset(0.0, 0.0)), 1); }); }); + group('$AnnotatedRegion findAll', () { + test('finds the first value in a OffsetLayer when sized', () { + final ContainerLayer containerLayer = ContainerLayer(); + final List layers = [ + OffsetLayer(offset: Offset.zero), + OffsetLayer(offset: const Offset(0.0, 100.0)), + OffsetLayer(offset: const Offset(0.0, 200.0)), + ]; + int i = 0; + for (OffsetLayer layer in layers) { + layer.append(AnnotatedRegionLayer(i, size: const Size(200.0, 100.0))); + containerLayer.append(layer); + i += 1; + } + + expect(containerLayer.findAll(const Offset(0.0, 1.0)), equals([0])); + expect(containerLayer.findAll(const Offset(0.0, 101.0)),equals([1])); + expect(containerLayer.findAll(const Offset(0.0, 201.0)), equals([2])); + }); + + test('finds a value within the clip in a ClipRectLayer', () { + final ContainerLayer containerLayer = ContainerLayer(); + final List layers = [ + ClipRectLayer(clipRect: const Rect.fromLTRB(0.0, 0.0, 100.0, 100.0)), + ClipRectLayer(clipRect: const Rect.fromLTRB(0.0, 100.0, 100.0, 200.0)), + ClipRectLayer(clipRect: const Rect.fromLTRB(0.0, 200.0, 100.0, 300.0)), + ]; + int i = 0; + for (ClipRectLayer layer in layers) { + layer.append(AnnotatedRegionLayer(i)); + containerLayer.append(layer); + i += 1; + } + + expect(containerLayer.findAll(const Offset(0.0, 1.0)), equals([0])); + expect(containerLayer.findAll(const Offset(0.0, 101.0)), equals([1])); + expect(containerLayer.findAll(const Offset(0.0, 201.0)), equals([2])); + }); + + + test('finds a value within the clip in a ClipRRectLayer', () { + final ContainerLayer containerLayer = ContainerLayer(); + final List layers = [ + ClipRRectLayer(clipRRect: RRect.fromLTRBR(0.0, 0.0, 100.0, 100.0, const Radius.circular(4.0))), + ClipRRectLayer(clipRRect: RRect.fromLTRBR(0.0, 100.0, 100.0, 200.0, const Radius.circular(4.0))), + ClipRRectLayer(clipRRect: RRect.fromLTRBR(0.0, 200.0, 100.0, 300.0, const Radius.circular(4.0))), + ]; + int i = 0; + for (ClipRRectLayer layer in layers) { + layer.append(AnnotatedRegionLayer(i)); + containerLayer.append(layer); + i += 1; + } + + expect(containerLayer.findAll(const Offset(5.0, 5.0)), equals([0])); + expect(containerLayer.findAll(const Offset(5.0, 105.0)), equals([1])); + expect(containerLayer.findAll(const Offset(5.0, 205.0)), equals([2])); + }); + + test('finds a value under a TransformLayer', () { + final Matrix4 transform = Matrix4( + 2.625, 0.0, 0.0, 0.0, + 0.0, 2.625, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + final TransformLayer transformLayer = TransformLayer(transform: transform); + final List layers = [ + OffsetLayer(), + OffsetLayer(offset: const Offset(0.0, 100.0)), + OffsetLayer(offset: const Offset(0.0, 200.0)), + ]; + int i = 0; + for (OffsetLayer layer in layers) { + final AnnotatedRegionLayer annotatedRegionLayer = AnnotatedRegionLayer(i, size: const Size(100.0, 100.0)); + layer.append(annotatedRegionLayer); + transformLayer.append(layer); + i += 1; + } + + expect(transformLayer.findAll(const Offset(0.0, 100.0)), equals([0])); + expect(transformLayer.findAll(const Offset(0.0, 200.0)), equals([0])); + expect(transformLayer.findAll(const Offset(0.0, 270.0)), equals([1])); + expect(transformLayer.findAll(const Offset(0.0, 400.0)), equals([1])); + expect(transformLayer.findAll(const Offset(0.0, 530.0)), equals([2])); + }); + + test('finds multiple nested, overlapping regions', () { + final ContainerLayer parent = ContainerLayer(); + + int index = 0; + final List> layers = >[ + AnnotatedRegionLayer(index++, size: const Size(100.0, 100.0)), + AnnotatedRegionLayer(index++, size: const Size(100.0, 100.0)), + ]; + for (ContainerLayer layer in layers) { + final AnnotatedRegionLayer annotatedRegionLayer = AnnotatedRegionLayer(index++, size: const Size(100.0, 100.0)); + layer.append(annotatedRegionLayer); + parent.append(layer); + } + + expect(parent.findAll(const Offset(0.0, 0.0)), equals([3, 1, 2, 0,])); + }); + + test('looks for child AnnotatedRegions before parents', () { + final AnnotatedRegionLayer parent = AnnotatedRegionLayer(1); + final AnnotatedRegionLayer child1 = AnnotatedRegionLayer(2); + final AnnotatedRegionLayer child2 = AnnotatedRegionLayer(3); + final AnnotatedRegionLayer child3 = AnnotatedRegionLayer(4); + final ContainerLayer layer = ContainerLayer(); + parent.append(child1); + parent.append(child2); + parent.append(child3); + layer.append(parent); + + expect(parent.findAll(Offset.zero), equals([4, 3, 2, 1])); + }); + + test('looks for correct type', () { + final AnnotatedRegionLayer child1 = AnnotatedRegionLayer(1); + final AnnotatedRegionLayer child2 = AnnotatedRegionLayer('hello'); + final ContainerLayer layer = ContainerLayer(); + layer.append(child2); + layer.append(child1); + + expect(layer.findAll(Offset.zero), equals(['hello'])); + }); + + test('does not clip Layer.find on an AnnotatedRegion with an unrelated type', () { + final AnnotatedRegionLayer child = AnnotatedRegionLayer(1); + final AnnotatedRegionLayer parent = AnnotatedRegionLayer('hello', size: const Size(10.0, 10.0)); + final ContainerLayer layer = ContainerLayer(); + parent.append(child); + layer.append(parent); + + expect(layer.findAll(const Offset(100.0, 100.0)), equals([1])); + }); + + test('handles non-invertable transforms', () { + final AnnotatedRegionLayer child = AnnotatedRegionLayer(1); + final TransformLayer parent = TransformLayer(transform: Matrix4.diagonal3Values(0.0, 1.0, 1.0)); + parent.append(child); + + expect(parent.findAll(const Offset(0.0, 0.0)), equals([])); + + parent.transform = Matrix4.diagonal3Values(1.0, 1.0, 1.0); + + expect(parent.findAll(const Offset(0.0, 0.0)), equals([1])); + }); + }); } diff --git a/packages/flutter/test/widgets/listener_test.dart b/packages/flutter/test/widgets/listener_test.dart index e244c4c40f..d563b4e6e2 100644 --- a/packages/flutter/test/widgets/listener_test.dart +++ b/packages/flutter/test/widgets/listener_test.dart @@ -135,7 +135,7 @@ void main() { expect(enter.position, equals(const Offset(400.0, 300.0))); expect(exit, isNull); }); - testWidgets('detects pointer exit', (WidgetTester tester) async { + testWidgets('detects pointer exiting', (WidgetTester tester) async { PointerEnterEvent enter; PointerHoverEvent move; PointerExitEvent exit; @@ -196,6 +196,87 @@ void main() { expect(exit.position, equals(const Offset(400.0, 300.0))); expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener.hoverAnnotation), isFalse); }); + testWidgets('Hover works with nested listeners', (WidgetTester tester) async { + final UniqueKey key1 = UniqueKey(); + final UniqueKey key2 = UniqueKey(); + final List enter1 = []; + final List move1 = []; + final List exit1 = []; + final List enter2 = []; + final List move2 = []; + final List exit2 = []; + void clearLists() { + enter1.clear(); + move1.clear(); + exit1.clear(); + enter2.clear(); + move2.clear(); + exit2.clear(); + } + + await tester.pumpWidget(Container()); + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.moveTo(const Offset(400.0, 0.0)); + await tester.pump(); + await tester.pumpWidget( + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Listener( + onPointerEnter: (PointerEnterEvent details) => enter1.add(details), + onPointerHover: (PointerHoverEvent details) => move1.add(details), + onPointerExit: (PointerExitEvent details) => exit1.add(details), + key: key1, + child: Container( + width: 200, + height: 200, + padding: const EdgeInsets.all(50.0), + child: Listener( + key: key2, + onPointerEnter: (PointerEnterEvent details) => enter2.add(details), + onPointerHover: (PointerHoverEvent details) => move2.add(details), + onPointerExit: (PointerExitEvent details) => exit2.add(details), + child: Container(), + ), + ), + ), + ], + ), + ); + final RenderPointerListener renderListener1 = tester.renderObject(find.byKey(key1)); + final RenderPointerListener renderListener2 = tester.renderObject(find.byKey(key2)); + Offset center = tester.getCenter(find.byKey(key2)); + await gesture.moveTo(center); + await tester.pump(); + expect(move2, isNotEmpty); + expect(enter2, isNotEmpty); + expect(exit2, isEmpty); + expect(move1, isNotEmpty); + expect(move1.last.position, equals(center)); + expect(enter1, isNotEmpty); + expect(enter1.last.position, equals(center)); + expect(exit1, isEmpty); + expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue); + 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. + center = center - const Offset(75.0, 0.0); + await gesture.moveTo(center); + await tester.pumpAndSettle(); + expect(move2, isEmpty); + expect(enter2, isEmpty); + expect(exit2, isNotEmpty); + expect(move1, isNotEmpty); + expect(move1.last.position, equals(center)); + expect(enter1, isEmpty); + expect(exit1, isEmpty); + expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue); + 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(); @@ -341,6 +422,7 @@ void main() { final Offset bottomLeft = tester.getBottomLeft(find.byKey(key)); expect(topRight.dx - topLeft.dx, scaleFactor * localWidth); expect(bottomLeft.dy - topLeft.dy, scaleFactor * localHeight); + print('Rect: ${tester.getRect(find.byKey(key))}'); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.moveTo(topLeft - const Offset(1, 1));