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

Fix nested listeners so that ancestor listeners can also receive enter/exit/move events. (#32350)

This changes Listener to trigger enter/move/exit in all Listeners below the pointer, not just the leaf region (the first region hit). This is because we need to allow listeners to be nested so that, say, a widget that handles changing color on hover, but also is wrapped in a Tooltip (that handles hover) can trigger both actions, not just one.

To that end, I added a findAll to Layer, similar to the existing find method that was previously used. It returns an iterator over annotated layers which match the given data type.

Since the findAll is implemented as returning an Iterable (and is sync*), I re-implemented the find routines as just returning the first result from findAll, since that should be just as efficient, and would then prevent duplication in the implementation.
parent e0943c42
......@@ -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<MouseTrackerAnnotation> 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,19 +229,20 @@ class MouseTracker extends ChangeNotifier {
for (int deviceId in _lastMouseEvent.keys) {
final PointerEvent lastEvent = _lastMouseEvent[deviceId];
final MouseTrackerAnnotation hit = annotationFinder(lastEvent.position);
final Iterable<MouseTrackerAnnotation> 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);
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.
......@@ -257,7 +258,7 @@ class MouseTracker extends ChangeNotifier {
// Tell any tracked annotations that weren't hit that they are no longer
// active.
for (_TrackedAnnotation trackedAnnotation in _trackedAnnotations.values) {
if (hitAnnotation == trackedAnnotation) {
if (hitAnnotations.contains(trackedAnnotation)) {
continue;
}
if (trackedAnnotation.activeDevices.contains(deviceId)) {
......@@ -269,6 +270,7 @@ class MouseTracker extends ChangeNotifier {
}
}
}
}
void _addMouseEvent(int deviceId, PointerEvent event) {
final bool wasConnected = mouseIsConnected;
......
......@@ -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<MouseTrackerAnnotation>(offset * window.devicePixelRatio);
return renderView.layer.findAll<MouseTrackerAnnotation>(offset * window.devicePixelRatio);
});
}
......
......@@ -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<S>(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<S> findAll<S>(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<S>(Offset regionOffset) => null;
@override
Iterable<S> findAll<S>(Offset regionOffset) => <S>[];
}
/// A composited layer that maps a backend texture to a rectangle.
......@@ -359,6 +375,9 @@ class TextureLayer extends Layer {
@override
S find<S>(Offset regionOffset) => null;
@override
Iterable<S> findAll<S>(Offset regionOffset) => <S>[];
}
/// 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<S>(Offset regionOffset) => null;
@override
Iterable<S> findAll<S>(Offset regionOffset) => <S>[];
}
/// A layer that indicates to the compositor that it should display
......@@ -468,6 +490,9 @@ class PerformanceOverlayLayer extends Layer {
@override
S find<S>(Offset regionOffset) => null;
@override
Iterable<S> findAll<S>(Offset regionOffset) => <S>[];
}
/// A composited layer that has a list of children.
......@@ -609,15 +634,24 @@ class ContainerLayer extends Layer {
@override
S find<S>(Offset regionOffset) {
Layer current = lastChild;
while (current != null) {
final Object value = current.find<S>(regionOffset);
if (value != null) {
return value;
final Iterable<S> all = findAll<S>(regionOffset);
if (all.isEmpty) {
return null;
}
current = current.previousSibling;
return all.first;
}
@override
Iterable<S> findAll<S>(Offset regionOffset) sync* {
if (firstChild == null)
return;
Layer child = lastChild;
while (true) {
yield* child.findAll<S>(regionOffset);
if (child == firstChild)
break;
child = child.previousSibling;
}
return null;
}
@override
......@@ -844,6 +878,11 @@ class OffsetLayer extends ContainerLayer {
return super.find<S>(regionOffset - offset);
}
@override
Iterable<S> findAll<S>(Offset regionOffset) {
return super.findAll<S>(regionOffset - offset);
}
@override
void applyTransform(Layer child, Matrix4 transform) {
assert(child != null);
......@@ -993,6 +1032,13 @@ class ClipRectLayer extends ContainerLayer {
return super.find<S>(regionOffset);
}
@override
Iterable<S> findAll<S>(Offset regionOffset) sync* {
if (!clipRect.contains(regionOffset))
return;
yield* super.findAll<S>(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<S>(regionOffset);
}
@override
Iterable<S> findAll<S>(Offset regionOffset) sync* {
if (!clipRRect.contains(regionOffset))
return;
yield* super.findAll<S>(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<S>(regionOffset);
}
@override
Iterable<S> findAll<S>(Offset regionOffset) sync* {
if (!clipPath.contains(regionOffset))
return;
yield* super.findAll<S>(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<S>(Offset regionOffset) {
final Iterable<S> all = findAll<S>(regionOffset);
if (all.isEmpty) {
return null;
}
return all.first;
}
@override
Iterable<S> findAll<S>(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<S>(Offset(result[0], result[1]));
yield* super.findAll<S>(Offset(result[0], result[1]));
}
@override
......@@ -1519,9 +1588,18 @@ class PhysicalModelLayer extends ContainerLayer {
@override
S find<S>(Offset regionOffset) {
if (!clipPath.contains(regionOffset))
final Iterable<S> all = findAll<S>(regionOffset);
if (all.isEmpty) {
return null;
return super.find<S>(regionOffset);
}
return all.first;
}
@override
Iterable<S> findAll<S>(Offset regionOffset) sync* {
if (!clipPath.contains(regionOffset))
return;
yield* super.findAll<S>(regionOffset);
}
@override
......@@ -1635,9 +1713,10 @@ class LeaderLayer extends ContainerLayer {
Offset _lastOffset;
@override
S find<S>(Offset regionOffset) {
return super.find<S>(regionOffset - offset);
}
S find<S>(Offset regionOffset) => super.find<S>(regionOffset - offset);
@override
Iterable<S> findAll<S>(Offset regionOffset) => super.findAll<S>(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<S>(Offset regionOffset) {
if (link.leader == null) {
return showWhenUnlinked ? super.find<S>(regionOffset - unlinkedOffset) : null;
}
Offset _transformOffset<S>(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<S>(Offset(result[0] - linkedOffset.dx, result[1] - linkedOffset.dy));
return Offset(result[0] - linkedOffset.dx, result[1] - linkedOffset.dy);
}
@override
S find<S>(Offset regionOffset) {
final Iterable<S> all = findAll<S>(regionOffset);
if (all.isEmpty) {
return null;
}
return all.first;
}
@override
Iterable<S> findAll<S>(Offset regionOffset) {
if (link.leader == null) {
return showWhenUnlinked ? super.findAll<S>(regionOffset - unlinkedOffset) : <S>[];
}
return super.findAll<S>(_transformOffset<S>(regionOffset));
}
/// The transform that was used during the last composition phase.
......@@ -1939,17 +2031,24 @@ class AnnotatedRegionLayer<T> extends ContainerLayer {
@override
S find<S>(Offset regionOffset) {
final S result = super.find<S>(regionOffset);
if (result != null)
return result;
if (size != null && !(offset & size).contains(regionOffset))
final Iterable<S> all = findAll<S>(regionOffset);
if (all.isEmpty) {
return null;
}
return all.first;
}
@override
Iterable<S> findAll<S>(Offset regionOffset) sync* {
yield* super.findAll<S>(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<S>(regionOffset);
}
@override
......
......@@ -60,6 +60,9 @@ class _ProxyLayer extends Layer {
@override
S find<S>(Offset regionOffset) => _layer.find(regionOffset);
@override
Iterable<S> findAll<S>(Offset regionOffset) => <S>[];
}
/// A [Canvas] that multicasts all method calls to a main canvas and a
......@@ -2799,6 +2802,9 @@ class _InspectorOverlayLayer extends Layer {
@override
S find<S>(Offset regionOffset) => null;
@override
Iterable<S> findAll<S>(Offset regionOffset) => <S>[];
}
const double _kScreenEdgeMargin = 10.0;
......
......@@ -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;
},
);
});
......
......@@ -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<OffsetLayer> layers = <OffsetLayer>[
......@@ -136,4 +136,154 @@ void main() {
expect(parent.find<int>(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<OffsetLayer> layers = <OffsetLayer>[
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<int>(i, size: const Size(200.0, 100.0)));
containerLayer.append(layer);
i += 1;
}
expect(containerLayer.findAll<int>(const Offset(0.0, 1.0)), equals(<int>[0]));
expect(containerLayer.findAll<int>(const Offset(0.0, 101.0)),equals(<int>[1]));
expect(containerLayer.findAll<int>(const Offset(0.0, 201.0)), equals(<int>[2]));
});
test('finds a value within the clip in a ClipRectLayer', () {
final ContainerLayer containerLayer = ContainerLayer();
final List<ClipRectLayer> layers = <ClipRectLayer>[
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<int>(i));
containerLayer.append(layer);
i += 1;
}
expect(containerLayer.findAll<int>(const Offset(0.0, 1.0)), equals(<int>[0]));
expect(containerLayer.findAll<int>(const Offset(0.0, 101.0)), equals(<int>[1]));
expect(containerLayer.findAll<int>(const Offset(0.0, 201.0)), equals(<int>[2]));
});
test('finds a value within the clip in a ClipRRectLayer', () {
final ContainerLayer containerLayer = ContainerLayer();
final List<ClipRRectLayer> layers = <ClipRRectLayer>[
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<int>(i));
containerLayer.append(layer);
i += 1;
}
expect(containerLayer.findAll<int>(const Offset(5.0, 5.0)), equals(<int>[0]));
expect(containerLayer.findAll<int>(const Offset(5.0, 105.0)), equals(<int>[1]));
expect(containerLayer.findAll<int>(const Offset(5.0, 205.0)), equals(<int>[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<OffsetLayer> layers = <OffsetLayer>[
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<int> annotatedRegionLayer = AnnotatedRegionLayer<int>(i, size: const Size(100.0, 100.0));
layer.append(annotatedRegionLayer);
transformLayer.append(layer);
i += 1;
}
expect(transformLayer.findAll<int>(const Offset(0.0, 100.0)), equals(<int>[0]));
expect(transformLayer.findAll<int>(const Offset(0.0, 200.0)), equals(<int>[0]));
expect(transformLayer.findAll<int>(const Offset(0.0, 270.0)), equals(<int>[1]));
expect(transformLayer.findAll<int>(const Offset(0.0, 400.0)), equals(<int>[1]));
expect(transformLayer.findAll<int>(const Offset(0.0, 530.0)), equals(<int>[2]));
});
test('finds multiple nested, overlapping regions', () {
final ContainerLayer parent = ContainerLayer();
int index = 0;
final List<AnnotatedRegionLayer<int>> layers = <AnnotatedRegionLayer<int>>[
AnnotatedRegionLayer<int>(index++, size: const Size(100.0, 100.0)),
AnnotatedRegionLayer<int>(index++, size: const Size(100.0, 100.0)),
];
for (ContainerLayer layer in layers) {
final AnnotatedRegionLayer<int> annotatedRegionLayer = AnnotatedRegionLayer<int>(index++, size: const Size(100.0, 100.0));
layer.append(annotatedRegionLayer);
parent.append(layer);
}
expect(parent.findAll<int>(const Offset(0.0, 0.0)), equals(<int>[3, 1, 2, 0,]));
});
test('looks for child AnnotatedRegions before parents', () {
final AnnotatedRegionLayer<int> parent = AnnotatedRegionLayer<int>(1);
final AnnotatedRegionLayer<int> child1 = AnnotatedRegionLayer<int>(2);
final AnnotatedRegionLayer<int> child2 = AnnotatedRegionLayer<int>(3);
final AnnotatedRegionLayer<int> child3 = AnnotatedRegionLayer<int>(4);
final ContainerLayer layer = ContainerLayer();
parent.append(child1);
parent.append(child2);
parent.append(child3);
layer.append(parent);
expect(parent.findAll<int>(Offset.zero), equals(<int>[4, 3, 2, 1]));
});
test('looks for correct type', () {
final AnnotatedRegionLayer<int> child1 = AnnotatedRegionLayer<int>(1);
final AnnotatedRegionLayer<String> child2 = AnnotatedRegionLayer<String>('hello');
final ContainerLayer layer = ContainerLayer();
layer.append(child2);
layer.append(child1);
expect(layer.findAll<String>(Offset.zero), equals(<String>['hello']));
});
test('does not clip Layer.find on an AnnotatedRegion with an unrelated type', () {
final AnnotatedRegionLayer<int> child = AnnotatedRegionLayer<int>(1);
final AnnotatedRegionLayer<String> parent = AnnotatedRegionLayer<String>('hello', size: const Size(10.0, 10.0));
final ContainerLayer layer = ContainerLayer();
parent.append(child);
layer.append(parent);
expect(layer.findAll<int>(const Offset(100.0, 100.0)), equals(<int>[1]));
});
test('handles non-invertable transforms', () {
final AnnotatedRegionLayer<int> child = AnnotatedRegionLayer<int>(1);
final TransformLayer parent = TransformLayer(transform: Matrix4.diagonal3Values(0.0, 1.0, 1.0));
parent.append(child);
expect(parent.findAll<int>(const Offset(0.0, 0.0)), equals(<int>[]));
parent.transform = Matrix4.diagonal3Values(1.0, 1.0, 1.0);
expect(parent.findAll<int>(const Offset(0.0, 0.0)), equals(<int>[1]));
});
});
}
......@@ -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<PointerEnterEvent> enter1 = <PointerEnterEvent>[];
final List<PointerHoverEvent> move1 = <PointerHoverEvent>[];
final List<PointerExitEvent> exit1 = <PointerExitEvent>[];
final List<PointerEnterEvent> enter2 = <PointerEnterEvent>[];
final List<PointerHoverEvent> move2 = <PointerHoverEvent>[];
final List<PointerExitEvent> exit2 = <PointerExitEvent>[];
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: <Widget>[
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));
......
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