Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/flutter/lib/src/rendering/mouse_tracker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ class MouseTracker extends ChangeNotifier {
/// The [updateWithEvent] is one of the two ways of updating mouse
/// states, the other one being [updateAllDevices].
void updateWithEvent(PointerEvent event, HitTestResult? hitTestResult) {
if (event.kind != PointerDeviceKind.mouse) {
if (event.kind != PointerDeviceKind.mouse && event.kind != PointerDeviceKind.stylus) {
return;
}
if (event is PointerSignalEvent) {
Expand Down
46 changes: 46 additions & 0 deletions packages/flutter/test/material/text_button_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2300,6 +2300,52 @@ void main() {
// The icon is aligned to the left of the button.
expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0); // 16.0 - padding between icon and button edge.
});

testWidgets('treats a hovering stylus like a mouse', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final ThemeData theme = ThemeData(useMaterial3: true);
bool hasBeenHovered = false;

await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Scaffold(
body: Center(
child: Builder(
builder: (BuildContext context) {
return TextButton(
onPressed: () {},
onHover: (bool entered) {
hasBeenHovered = true;
},
focusNode: focusNode,
child: const Text('TextButton'),
);
},
),
),
),
),
);

RenderObject overlayColor() {
return tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
}

final Offset center = tester.getCenter(find.byType(TextButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.stylus,
);
await gesture.addPointer();
await tester.pumpAndSettle();

expect(hasBeenHovered, isFalse);

await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(overlayColor(), paints..rect(color: theme.colorScheme.primary.withOpacity(0.08)));
expect(hasBeenHovered, isTrue);
});
}

TextStyle? _iconStyle(WidgetTester tester, IconData icon) {
Expand Down
133 changes: 77 additions & 56 deletions packages/flutter/test/rendering/mouse_tracker_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,63 +72,81 @@ void main() {

final Matrix4 translate10by20 = Matrix4.translationValues(10, 20, 0);

test('should detect enter, hover, and exit from Added, Hover, and Removed events', () {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard to read the diff here, but all I did was wrap this in a for so that it's run once for mouse gestures and once for stylus.

final List<PointerEvent> events = <PointerEvent>[];
setUpWithOneAnnotation(logEvents: events);
for (final ui.PointerDeviceKind pointerDeviceKind in <ui.PointerDeviceKind>[ui.PointerDeviceKind.mouse, ui.PointerDeviceKind.stylus]) {
test('should detect enter, hover, and exit from Added, Hover, and Removed events for stylus', () {
final List<PointerEvent> events = <PointerEvent>[];
setUpWithOneAnnotation(logEvents: events);

final List<bool> listenerLogs = <bool>[];
_mouseTracker.addListener(() {
listenerLogs.add(_mouseTracker.mouseIsConnected);
});

final List<bool> listenerLogs = <bool>[];
_mouseTracker.addListener(() {
listenerLogs.add(_mouseTracker.mouseIsConnected);
expect(_mouseTracker.mouseIsConnected, isFalse);

// Pointer enters the annotation.
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(
PointerChange.add,
Offset.zero,
kind: pointerDeviceKind,
),
]));
addTearDown(() => dispatchRemoveDevice());

expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerEnterEvent>(const PointerEnterEvent()),
]));
expect(listenerLogs, <bool>[true]);
events.clear();
listenerLogs.clear();

// Pointer hovers the annotation.
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(
PointerChange.hover,
const Offset(1.0, 101.0),
kind: pointerDeviceKind,
),
]));
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerHoverEvent>(const PointerHoverEvent(position: Offset(1.0, 101.0))),
]));
expect(_mouseTracker.mouseIsConnected, isTrue);
expect(listenerLogs, isEmpty);
events.clear();

// Pointer is removed while on the annotation.
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(
PointerChange.remove,
const Offset(1.0, 101.0),
kind: pointerDeviceKind,
),
]));
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerExitEvent>(const PointerExitEvent(position: Offset(1.0, 101.0))),
]));
expect(listenerLogs, <bool>[false]);
events.clear();
listenerLogs.clear();

// Pointer is added on the annotation.
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(
PointerChange.add,
const Offset(0.0, 301.0),
kind: pointerDeviceKind,
),
]));
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerEnterEvent>(const PointerEnterEvent(position: Offset(0.0, 301.0))),
]));
expect(listenerLogs, <bool>[true]);
events.clear();
listenerLogs.clear();
});

expect(_mouseTracker.mouseIsConnected, isFalse);

// Pointer enters the annotation.
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, Offset.zero),
]));
addTearDown(() => dispatchRemoveDevice());

expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerEnterEvent>(const PointerEnterEvent()),
]));
expect(listenerLogs, <bool>[true]);
events.clear();
listenerLogs.clear();

// Pointer hovers the annotation.
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
]));
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerHoverEvent>(const PointerHoverEvent(position: Offset(1.0, 101.0))),
]));
expect(_mouseTracker.mouseIsConnected, isTrue);
expect(listenerLogs, isEmpty);
events.clear();

// Pointer is removed while on the annotation.
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.remove, const Offset(1.0, 101.0)),
]));
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerExitEvent>(const PointerExitEvent(position: Offset(1.0, 101.0))),
]));
expect(listenerLogs, <bool>[false]);
events.clear();
listenerLogs.clear();

// Pointer is added on the annotation.
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 301.0)),
]));
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerEnterEvent>(const PointerEnterEvent(position: Offset(0.0, 301.0))),
]));
expect(listenerLogs, <bool>[true]);
events.clear();
listenerLogs.clear();
});
}

// Regression test for https://github.com/flutter/flutter/issues/90838
test('should not crash if the first event is a Removed event', () {
Expand Down Expand Up @@ -623,7 +641,10 @@ class BaseEventMatcher extends Matcher {
bool matches(dynamic untypedItem, Map<dynamic, dynamic> matchState) {
final PointerEvent actual = untypedItem as PointerEvent;
if (!(
_matchesField(matchState, 'kind', actual.kind, PointerDeviceKind.mouse) &&
(
_matchesField(matchState, 'kind', actual.kind, PointerDeviceKind.mouse) ||
_matchesField(matchState, 'kind', actual.kind, PointerDeviceKind.stylus)
) &&
_matchesField(matchState, 'position', actual.position, expected.position) &&
_matchesField(matchState, 'device', actual.device, expected.device) &&
_matchesField(matchState, 'localPosition', actual.localPosition, expected.localPosition)
Expand Down
41 changes: 41 additions & 0 deletions packages/flutter/test/widgets/mouse_region_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1868,6 +1868,47 @@ void main() {
await gesture.cancel();
expect(tester.takeException(), isNull);
});

testWidgets('stylus input works', (WidgetTester tester) async {
bool onEnter = false;
bool onExit = false;
bool onHover = false;
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: MouseRegion(
onEnter: (_) => onEnter = true,
onExit: (_) => onExit = true,
onHover: (_) => onHover = true,
child: const SizedBox(
width: 10.0,
height: 10.0,
),
),
),
));

final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.stylus);
await gesture.addPointer(location: const Offset(20.0, 20.0));
await tester.pump();

expect(onEnter, false);
expect(onHover, false);
expect(onExit, false);

await gesture.moveTo(const Offset(5.0, 5.0));
await tester.pump();

expect(onEnter, true);
expect(onHover, true);
expect(onExit, false);

await gesture.moveTo(const Offset(20.0, 20.0));
await tester.pump();

expect(onEnter, true);
expect(onHover, true);
expect(onExit, true);
});
}

// Render widget `topLeft` at the top-left corner, stacking on top of the widget
Expand Down