Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 114f7dd

Browse files
authored
Multiple touches of a stylus should be considered from the same device (#56075)
This PR fixes flutter/flutter#156223. **Problem:** The issue above only occurred when using stylus on Web. Multiple touches were treated as different devices: new devices were added and former devices that had the pointer lifted were never removed. This was further caused by the design that Flutter uses JS's `PointerEvent.pointerId` as the device ID, which increases for each touch. Flutter had to use `PointerEvent.pointerId` because JS doesn't provide a way to distinguish between multiple styluses. This PR fixes this issue by simply supporting only one stylus, since support for multi-stylus can be really hard. How multi-stylus should be supported can be discussed upon such demands in the future, but at least for now, iPad doesn't support multi-stylus and it's not something I can test. This PR also rewrote some comments that I got confused by while reading the code. [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
1 parent abeb875 commit 114f7dd

File tree

2 files changed

+158
-11
lines changed

2 files changed

+158
-11
lines changed

lib/web_ui/lib/src/engine/pointer_binding.dart

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,27 @@ typedef _PointerDataCallback = void Function(DomEvent event, List<ui.PointerData
3737
// here, we use an already very large number (30 bits).
3838
const int _kButtonsMask = 0x3FFFFFFF;
3939

40-
// Intentionally set to -1 or -2 so it doesn't conflict with other device IDs.
40+
// Assumes the device supports at most one mouse, one touch screen, and one
41+
// trackpad, therefore these pointer events are assigned fixed device IDs.
4142
const int _mouseDeviceId = -1;
4243
const int _trackpadDeviceId = -2;
44+
// For now only one stylus is supported.
45+
//
46+
// Device may support multiple styluses, but `PointerEvent` does not
47+
// distinguish between them with unique identifiers. Additionally, repeated
48+
// touches from the same stylus will be assigned different `pointerId`s each
49+
// time. Since it's really hard to handle, support for multiple styluses is
50+
// left for when demanded.
51+
const int _stylusDeviceId = -4;
4352

4453
const int _kPrimaryMouseButton = 0x1;
4554
const int _kSecondaryMouseButton = 0x2;
46-
const int _kMiddleMouseButton =0x4;
55+
const int _kMiddleMouseButton = 0x4;
4756

4857
int _nthButton(int n) => 0x1 << n;
4958

50-
/// Convert the `button` property of PointerEvent or MouseEvent to a bit mask of
51-
/// its `buttons` property.
59+
/// Convert the `button` property of PointerEvent to a bit mask of its `buttons`
60+
/// property.
5261
///
5362
/// The `button` property is a integer describing the button changed in an event,
5463
/// which is sequentially 0 for LMB, 1 for MMB, 2 for RMB, 3 for backward and
@@ -1009,7 +1018,19 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
10091018
}
10101019
});
10111020

1012-
// Why `domWindow` you ask? See this fiddle: https://jsfiddle.net/ditman/7towxaqp
1021+
// Move event listeners should be added to `_globalTarget` instead of
1022+
// `_viewTarget`. This is because `_viewTarget` (the root) captures pointers
1023+
// by default, meaning a pointer that starts within `_viewTarget` continues
1024+
// sending move events to its listener even when dragged outside.
1025+
//
1026+
// In contrast, `_globalTarget` (a regular <div>) stops sending move events
1027+
// when the pointer moves outside its bounds and resumes them only when the
1028+
// pointer re-enters.
1029+
//
1030+
// For demonstration, see this fiddle: https://jsfiddle.net/ditman/7towxaqp
1031+
//
1032+
// TODO(dkwingsmt): Investigate whether we can configure the behavior for
1033+
// `_viewTarget`. https://github.com/flutter/flutter/issues/157968
10131034
_addPointerEventListener(_globalTarget, 'pointermove', (DomPointerEvent event) {
10141035
final int device = _getPointerId(event);
10151036
final _ButtonSanitizer sanitizer = _ensureSanitizer(device);
@@ -1133,12 +1154,21 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
11331154
}
11341155

11351156
int _getPointerId(DomPointerEvent event) {
1136-
// We force `device: _mouseDeviceId` on mouse pointers because Wheel events
1137-
// might come before any PointerEvents, and since wheel events don't contain
1138-
// pointerId we always assign `device: _mouseDeviceId` to them.
1139-
final ui.PointerDeviceKind kind = _pointerTypeToDeviceKind(event.pointerType!);
1140-
return kind == ui.PointerDeviceKind.mouse ? _mouseDeviceId :
1141-
event.pointerId!.toInt();
1157+
// All mouse pointer events are given `_mouseDeviceId`, including wheel
1158+
// events, because wheel events might come before any other PointerEvents,
1159+
// and wheel PointerEvents don't contain pointerIds.
1160+
return switch(_pointerTypeToDeviceKind(event.pointerType!)) {
1161+
ui.PointerDeviceKind.mouse => _mouseDeviceId,
1162+
1163+
ui.PointerDeviceKind.stylus ||
1164+
ui.PointerDeviceKind.invertedStylus => _stylusDeviceId,
1165+
1166+
// Trackpad processing doesn't call this function.
1167+
ui.PointerDeviceKind.trackpad => throw Exception('Unreachable'),
1168+
1169+
ui.PointerDeviceKind.touch ||
1170+
ui.PointerDeviceKind.unknown => event.pointerId!.toInt(),
1171+
};
11421172
}
11431173

11441174
/// Tilt angle is -90 to + 90. Take maximum deflection and convert to radians.

lib/web_ui/test/engine/pointer_binding_test.dart

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2358,6 +2358,89 @@ void testMain() {
23582358
},
23592359
);
23602360

2361+
// STYLUS
2362+
2363+
test(
2364+
'handles stylus touches',
2365+
() {
2366+
// Repeated stylus touches use different pointerIds.
2367+
2368+
final _PointerEventContext context = _PointerEventContext();
2369+
2370+
final List<ui.PointerDataPacket> packets = <ui.PointerDataPacket>[];
2371+
ui.PlatformDispatcher.instance.onPointerDataPacket = (ui.PointerDataPacket packet) {
2372+
packets.add(packet);
2373+
};
2374+
2375+
rootElement.dispatchEvent(context.stylusTouchDown(
2376+
pointerId: 100,
2377+
buttons: 1,
2378+
clientX: 5.0,
2379+
clientY: 100.0,
2380+
));
2381+
expect(packets, hasLength(1));
2382+
expect(packets[0].data, hasLength(2));
2383+
expect(packets[0].data[0].change, equals(ui.PointerChange.add));
2384+
expect(packets[0].data[0].synthesized, isTrue);
2385+
expect(packets[0].data[1].change, equals(ui.PointerChange.down));
2386+
expect(packets[0].data[1].synthesized, isFalse);
2387+
expect(packets[0].data[1].buttons, equals(1));
2388+
expect(packets[0].data[1].physicalX, equals(5.0 * dpi));
2389+
expect(packets[0].data[1].physicalY, equals(100.0 * dpi));
2390+
packets.clear();
2391+
2392+
rootElement.dispatchEvent(context.stylusTouchUp(
2393+
pointerId: 100,
2394+
buttons: 0,
2395+
clientX: 5.0,
2396+
clientY: 100.0,
2397+
));
2398+
expect(packets, hasLength(1));
2399+
expect(packets[0].data, hasLength(1));
2400+
expect(packets[0].data[0].change, equals(ui.PointerChange.up));
2401+
expect(packets[0].data[0].synthesized, isFalse);
2402+
expect(packets[0].data[0].buttons, equals(0));
2403+
expect(packets[0].data[0].physicalX, equals(5.0 * dpi));
2404+
expect(packets[0].data[0].physicalY, equals(100.0 * dpi));
2405+
packets.clear();
2406+
2407+
rootElement.dispatchEvent(context.stylusTouchDown(
2408+
pointerId: 101,
2409+
buttons: 1,
2410+
clientX: 5.0,
2411+
clientY: 150.0,
2412+
));
2413+
expect(packets, hasLength(1));
2414+
expect(packets[0].data, hasLength(2));
2415+
expect(packets[0].data[0].change, equals(ui.PointerChange.hover));
2416+
expect(packets[0].data[0].synthesized, isTrue);
2417+
expect(packets[0].data[0].buttons, equals(0));
2418+
expect(packets[0].data[0].physicalX, equals(5.0 * dpi));
2419+
expect(packets[0].data[0].physicalY, equals(150.0 * dpi));
2420+
expect(packets[0].data[1].change, equals(ui.PointerChange.down));
2421+
expect(packets[0].data[1].synthesized, isFalse);
2422+
expect(packets[0].data[1].buttons, equals(1));
2423+
expect(packets[0].data[1].physicalX, equals(5.0 * dpi));
2424+
expect(packets[0].data[1].physicalY, equals(150.0 * dpi));
2425+
packets.clear();
2426+
2427+
rootElement.dispatchEvent(context.stylusTouchUp(
2428+
pointerId: 101,
2429+
buttons: 0,
2430+
clientX: 5.0,
2431+
clientY: 150.0,
2432+
));
2433+
expect(packets, hasLength(1));
2434+
expect(packets[0].data, hasLength(1));
2435+
expect(packets[0].data[0].change, equals(ui.PointerChange.up));
2436+
expect(packets[0].data[0].synthesized, isFalse);
2437+
expect(packets[0].data[0].buttons, equals(0));
2438+
expect(packets[0].data[0].physicalX, equals(5.0 * dpi));
2439+
expect(packets[0].data[0].physicalY, equals(150.0 * dpi));
2440+
packets.clear();
2441+
},
2442+
);
2443+
23612444
// MULTIPOINTER ADAPTERS
23622445

23632446
test(
@@ -3600,6 +3683,40 @@ class _PointerEventContext extends _BasicEventContext
36003683
}))
36013684
.toList();
36023685
}
3686+
3687+
// STYLUSES
3688+
3689+
DomEvent stylusTouchDown({
3690+
double? clientX,
3691+
double? clientY,
3692+
int? buttons,
3693+
int? pointerId = 1000,
3694+
}) {
3695+
return _downWithFullDetails(
3696+
pointer: pointerId,
3697+
buttons: buttons,
3698+
button: 0,
3699+
clientX: clientX,
3700+
clientY: clientY,
3701+
pointerType: 'pen',
3702+
);
3703+
}
3704+
3705+
DomEvent stylusTouchUp({
3706+
double? clientX,
3707+
double? clientY,
3708+
int? buttons,
3709+
int? pointerId = 1000,
3710+
}) {
3711+
return _upWithFullDetails(
3712+
pointer: pointerId,
3713+
buttons: buttons,
3714+
button: 0,
3715+
clientX: clientX,
3716+
clientY: clientY,
3717+
pointerType: 'pen',
3718+
);
3719+
}
36033720
}
36043721

36053722
class MockPointerSupportDetector implements PointerSupportDetector {

0 commit comments

Comments
 (0)