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

Multiple touches of a stylus should be considered from the same device #56075

Merged
merged 9 commits into from
Nov 2, 2024
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
52 changes: 41 additions & 11 deletions lib/web_ui/lib/src/engine/pointer_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,27 @@ typedef _PointerDataCallback = void Function(DomEvent event, List<ui.PointerData
// here, we use an already very large number (30 bits).
const int _kButtonsMask = 0x3FFFFFFF;

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

const int _kPrimaryMouseButton = 0x1;
const int _kSecondaryMouseButton = 0x2;
const int _kMiddleMouseButton =0x4;
const int _kMiddleMouseButton = 0x4;

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

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

// Why `domWindow` you ask? See this fiddle: https://jsfiddle.net/ditman/7towxaqp
// Move event listeners should be added to `_globalTarget` instead of
// `_viewTarget`. This is because `_viewTarget` (the root) captures pointers
// by default, meaning a pointer that starts within `_viewTarget` continues
// sending move events to its listener even when dragged outside.
Comment on lines +1021 to +1024
Copy link
Member

Choose a reason for hiding this comment

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

I haven't revisited this yet, BUT, there's a way to have the behavior that we needed _globalTarget for from the _viewTarget!!

See this: https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, that's what I thought as well. Maybe you can change it in a future PR :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've added a TODO with issue flutter/flutter#157968

//
// In contrast, `_globalTarget` (a regular <div>) stops sending move events
// when the pointer moves outside its bounds and resumes them only when the
// pointer re-enters.
//
// For demonstration, see this fiddle: https://jsfiddle.net/ditman/7towxaqp
//
// TODO(dkwingsmt): Investigate whether we can configure the behavior for
// `_viewTarget`. https://github.com/flutter/flutter/issues/157968
_addPointerEventListener(_globalTarget, 'pointermove', (DomPointerEvent event) {
final int device = _getPointerId(event);
final _ButtonSanitizer sanitizer = _ensureSanitizer(device);
Expand Down Expand Up @@ -1133,12 +1154,21 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
}

int _getPointerId(DomPointerEvent event) {
// We force `device: _mouseDeviceId` on mouse pointers because Wheel events
// might come before any PointerEvents, and since wheel events don't contain
// pointerId we always assign `device: _mouseDeviceId` to them.
final ui.PointerDeviceKind kind = _pointerTypeToDeviceKind(event.pointerType!);
return kind == ui.PointerDeviceKind.mouse ? _mouseDeviceId :
event.pointerId!.toInt();
// All mouse pointer events are given `_mouseDeviceId`, including wheel
// events, because wheel events might come before any other PointerEvents,
// and wheel PointerEvents don't contain pointerIds.
return switch(_pointerTypeToDeviceKind(event.pointerType!)) {
ui.PointerDeviceKind.mouse => _mouseDeviceId,

ui.PointerDeviceKind.stylus ||
ui.PointerDeviceKind.invertedStylus => _stylusDeviceId,

// Trackpad processing doesn't call this function.
ui.PointerDeviceKind.trackpad => throw Exception('Unreachable'),

ui.PointerDeviceKind.touch ||
ui.PointerDeviceKind.unknown => event.pointerId!.toInt(),
};
}

/// Tilt angle is -90 to + 90. Take maximum deflection and convert to radians.
Expand Down
117 changes: 117 additions & 0 deletions lib/web_ui/test/engine/pointer_binding_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2358,6 +2358,89 @@ void testMain() {
},
);

// STYLUS

test(
'handles stylus touches',
() {
// Repeated stylus touches use different pointerIds.

final _PointerEventContext context = _PointerEventContext();

final List<ui.PointerDataPacket> packets = <ui.PointerDataPacket>[];
ui.PlatformDispatcher.instance.onPointerDataPacket = (ui.PointerDataPacket packet) {
packets.add(packet);
};

rootElement.dispatchEvent(context.stylusTouchDown(
pointerId: 100,
buttons: 1,
clientX: 5.0,
clientY: 100.0,
));
expect(packets, hasLength(1));
expect(packets[0].data, hasLength(2));
expect(packets[0].data[0].change, equals(ui.PointerChange.add));
expect(packets[0].data[0].synthesized, isTrue);
expect(packets[0].data[1].change, equals(ui.PointerChange.down));
expect(packets[0].data[1].synthesized, isFalse);
expect(packets[0].data[1].buttons, equals(1));
expect(packets[0].data[1].physicalX, equals(5.0 * dpi));
expect(packets[0].data[1].physicalY, equals(100.0 * dpi));
packets.clear();

rootElement.dispatchEvent(context.stylusTouchUp(
pointerId: 100,
buttons: 0,
clientX: 5.0,
clientY: 100.0,
));
expect(packets, hasLength(1));
expect(packets[0].data, hasLength(1));
expect(packets[0].data[0].change, equals(ui.PointerChange.up));
expect(packets[0].data[0].synthesized, isFalse);
expect(packets[0].data[0].buttons, equals(0));
expect(packets[0].data[0].physicalX, equals(5.0 * dpi));
expect(packets[0].data[0].physicalY, equals(100.0 * dpi));
packets.clear();

rootElement.dispatchEvent(context.stylusTouchDown(
pointerId: 101,
buttons: 1,
clientX: 5.0,
clientY: 150.0,
));
expect(packets, hasLength(1));
expect(packets[0].data, hasLength(2));
expect(packets[0].data[0].change, equals(ui.PointerChange.hover));
expect(packets[0].data[0].synthesized, isTrue);
expect(packets[0].data[0].buttons, equals(0));
expect(packets[0].data[0].physicalX, equals(5.0 * dpi));
expect(packets[0].data[0].physicalY, equals(150.0 * dpi));
expect(packets[0].data[1].change, equals(ui.PointerChange.down));
expect(packets[0].data[1].synthesized, isFalse);
expect(packets[0].data[1].buttons, equals(1));
expect(packets[0].data[1].physicalX, equals(5.0 * dpi));
expect(packets[0].data[1].physicalY, equals(150.0 * dpi));
packets.clear();

rootElement.dispatchEvent(context.stylusTouchUp(
pointerId: 101,
buttons: 0,
clientX: 5.0,
clientY: 150.0,
));
expect(packets, hasLength(1));
expect(packets[0].data, hasLength(1));
expect(packets[0].data[0].change, equals(ui.PointerChange.up));
expect(packets[0].data[0].synthesized, isFalse);
expect(packets[0].data[0].buttons, equals(0));
expect(packets[0].data[0].physicalX, equals(5.0 * dpi));
expect(packets[0].data[0].physicalY, equals(150.0 * dpi));
packets.clear();
},
);

// MULTIPOINTER ADAPTERS

test(
Expand Down Expand Up @@ -3600,6 +3683,40 @@ class _PointerEventContext extends _BasicEventContext
}))
.toList();
}

// STYLUSES

DomEvent stylusTouchDown({
double? clientX,
double? clientY,
int? buttons,
int? pointerId = 1000,
}) {
return _downWithFullDetails(
pointer: pointerId,
buttons: buttons,
button: 0,
clientX: clientX,
clientY: clientY,
pointerType: 'pen',
);
}

DomEvent stylusTouchUp({
double? clientX,
double? clientY,
int? buttons,
int? pointerId = 1000,
}) {
return _upWithFullDetails(
pointer: pointerId,
buttons: buttons,
button: 0,
clientX: clientX,
clientY: clientY,
pointerType: 'pen',
);
}
}

class MockPointerSupportDetector implements PointerSupportDetector {
Expand Down