winit-core:winit: add tablet input support

The API is integrated into the `WindowEvent::Pointer*` API and is
present in form of `TabletTool` variant on corresponding data entries.

For now implemented for Web, Windows, and with limitations for Wayland.

Fixes #99.

Co-authored-by: daxpedda <daxpedda@gmail.com>
This commit is contained in:
Kirill Chibisov 2025-10-07 21:42:36 +09:00 committed by GitHub
parent ffcdf80192
commit f046e778aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1206 additions and 204 deletions

View file

@ -216,7 +216,7 @@ impl CursorHandler {
Cursor::Custom(cursor) => {
let cursor = match cursor.cast_ref::<CustomCursor>() {
Some(cursor) => cursor,
None => todo!(),
None => return,
};
if let SelectedCursor::Loading { cursor: old_cursor, .. }

View file

@ -24,7 +24,7 @@ use crate::event_loop::ActiveEventLoop;
use crate::main_thread::MainThreadMarker;
use crate::monitor::MonitorHandler;
use crate::r#async::DispatchRunner;
use crate::web_sys::event::mouse_button_to_id;
use crate::web_sys::event::ButtonsState;
use crate::window::Inner;
use crate::{backend, event, EventLoop, PollStrategy, WaitUntilStrategy};
@ -318,8 +318,10 @@ impl Shared {
// chorded button event
let device_id = event::mkdid(event.pointer_id());
if let Some(button) = backend::event::mouse_button(&event) {
let state = if backend::event::mouse_buttons(&event).contains(button.into()) {
if let Some(button) = backend::event::raw_button(&event) {
let state = if backend::event::pointer_buttons(&event)
.contains(ButtonsState::from_bits_retain(button))
{
ElementState::Pressed
} else {
ElementState::Released
@ -327,10 +329,7 @@ impl Shared {
runner.send_event(Event::DeviceEvent {
device_id,
event: DeviceEvent::Button {
button: mouse_button_to_id(button).into(),
state,
},
event: DeviceEvent::Button { button: button.into(), state },
});
return;
@ -338,14 +337,16 @@ impl Shared {
// pointer move event
let mut delta = backend::event::MouseDelta::init(&navigator, &event);
runner.send_events(backend::event::pointer_move_event(event).map(|event| {
let delta = delta.delta(&event).to_physical(backend::scale_factor(&window));
runner.send_events(backend::event::pointer_move_event(event).map(
|event: web_sys::PointerEvent| {
let delta = delta.delta(&event).to_physical(backend::scale_factor(&window));
Event::DeviceEvent {
device_id,
event: DeviceEvent::PointerMotion { delta: (delta.x, delta.y) },
}
}));
Event::DeviceEvent {
device_id,
event: DeviceEvent::PointerMotion { delta: (delta.x, delta.y) },
}
},
));
}),
));
let runner = self.clone();
@ -375,11 +376,11 @@ impl Shared {
return;
}
let button = backend::event::mouse_button(&event).expect("no mouse button pressed");
let button = backend::event::raw_button(&event).expect("no pointer button pressed");
runner.send_event(Event::DeviceEvent {
device_id: event::mkdid(event.pointer_id()),
event: DeviceEvent::Button {
button: mouse_button_to_id(button).into(),
button: button.into(),
state: ElementState::Pressed,
},
});
@ -394,11 +395,11 @@ impl Shared {
return;
}
let button = backend::event::mouse_button(&event).expect("no mouse button pressed");
let button = backend::event::raw_button(&event).expect("no pointer button pressed");
runner.send_event(Event::DeviceEvent {
device_id: event::mkdid(event.pointer_id()),
event: DeviceEvent::Button {
button: mouse_button_to_id(button).into(),
button: button.into(),
state: ElementState::Released,
},
});

View file

@ -1,11 +1,16 @@
use std::cell::OnceCell;
use std::f64;
use dpi::{LogicalPosition, PhysicalPosition, Position};
use smol_str::SmolStr;
use tracing::warn;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{KeyboardEvent, MouseEvent, Navigator, PointerEvent, WheelEvent};
use winit_core::event::{FingerId, MouseButton, MouseScrollDelta, PointerKind};
use web_sys::{Event, KeyboardEvent, MouseEvent, Navigator, PointerEvent, WheelEvent};
use winit_core::event::{
ButtonSource, FingerId, Force, MouseButton, MouseScrollDelta, PointerKind, PointerSource,
TabletToolAngle, TabletToolButton, TabletToolData, TabletToolKind, TabletToolTilt,
};
use winit_core::keyboard::{
Key, KeyCode, KeyLocation, ModifiersState, NamedKey, NativeKey, NativeKeyCode, PhysicalKey,
};
@ -16,11 +21,12 @@ bitflags::bitflags! {
// https://www.w3.org/TR/pointerevents3/#the-buttons-property
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ButtonsState: u16 {
const LEFT = 0b00001;
const RIGHT = 0b00010;
const MIDDLE = 0b00100;
const BACK = 0b01000;
const FORWARD = 0b10000;
const LEFT = 0b000001;
const RIGHT = 0b000010;
const MIDDLE = 0b000100;
const BACK = 0b001000;
const FORWARD = 0b010000;
const ERASER = 0b100000;
}
}
@ -37,6 +43,15 @@ impl From<ButtonsState> for MouseButton {
}
}
impl From<ButtonSource> for ButtonsState {
fn from(value: ButtonSource) -> Self {
match value {
ButtonSource::TabletTool { button, .. } => button.into(),
other => ButtonsState::from(other.mouse_button()),
}
}
}
impl From<MouseButton> for ButtonsState {
fn from(value: MouseButton) -> Self {
match value {
@ -50,37 +65,133 @@ impl From<MouseButton> for ButtonsState {
}
}
pub fn mouse_buttons(event: &MouseEvent) -> ButtonsState {
impl From<TabletToolButton> for ButtonsState {
fn from(tool: TabletToolButton) -> Self {
match tool {
TabletToolButton::Contact => ButtonsState::LEFT,
TabletToolButton::Barrel => ButtonsState::RIGHT,
TabletToolButton::Other(value) => Self::from_bits_retain(value),
}
}
}
pub fn pointer_buttons(event: &MouseEvent) -> ButtonsState {
#[allow(clippy::disallowed_methods)]
ButtonsState::from_bits_retain(event.buttons())
}
pub fn mouse_button(event: &MouseEvent) -> Option<MouseButton> {
pub fn raw_button(event: &MouseEvent) -> Option<u16> {
// https://www.w3.org/TR/pointerevents3/#the-button-property
match event.button() {
-1 => None,
0 => Some(MouseButton::Left),
1 => Some(MouseButton::Middle),
2 => Some(MouseButton::Right),
3 => Some(MouseButton::Back),
4 => Some(MouseButton::Forward),
i => {
Some(MouseButton::Other(i.try_into().expect("unexpected negative mouse button value")))
},
#[allow(clippy::disallowed_methods)]
let button = event.button();
if button == -1 {
None
} else {
Some(button.try_into().expect("unexpected negative mouse button value"))
}
}
pub fn mouse_button_to_id(button: MouseButton) -> u16 {
pub fn mouse_button(button: u16) -> MouseButton {
match button {
MouseButton::Left => 0,
MouseButton::Right => 1,
MouseButton::Middle => 2,
MouseButton::Back => 3,
MouseButton::Forward => 4,
MouseButton::Other(value) => value,
0 => MouseButton::Left,
1 => MouseButton::Middle,
2 => MouseButton::Right,
3 => MouseButton::Back,
4 => MouseButton::Forward,
other => MouseButton::Other(other),
}
}
pub fn mouse_position(event: &MouseEvent) -> LogicalPosition<f64> {
pub fn tool_button(button: u16) -> TabletToolButton {
match button {
0 => TabletToolButton::Contact,
2 => TabletToolButton::Barrel,
other => TabletToolButton::Other(other),
}
}
#[derive(Clone, Copy)]
pub enum WebPointerType {
Mouse,
Touch,
Pen,
}
impl WebPointerType {
pub fn from_event(event: &PointerEvent) -> Option<Self> {
#[allow(clippy::disallowed_methods)]
let r#type = event.pointer_type();
match r#type.as_ref() {
"mouse" => Some(Self::Mouse),
"touch" => Some(Self::Touch),
"pen" => Some(Self::Pen),
r#type => {
warn!("found unknown pointer type: {type}");
None
},
}
}
}
pub fn pointer_kind(event: &PointerEvent, pointer_id: i32) -> PointerKind {
match WebPointerType::from_event(event) {
Some(WebPointerType::Mouse) => PointerKind::Mouse,
Some(WebPointerType::Touch) => PointerKind::Touch(FingerId::from_raw(pointer_id as usize)),
Some(WebPointerType::Pen) => {
PointerKind::TabletTool(if pointer_buttons(event).contains(ButtonsState::ERASER) {
TabletToolKind::Eraser
} else {
TabletToolKind::Pen
})
},
None => PointerKind::Unknown,
}
}
pub fn pointer_source(event: &PointerEvent, kind: PointerKind) -> PointerSource {
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(extends = PointerEvent, extends = MouseEvent, extends = Event)]
pub type PointerEventExt;
#[wasm_bindgen(method, getter, js_name = altitudeAngle)]
pub fn altitude_angle(this: &PointerEventExt) -> Option<f64>;
#[wasm_bindgen(method, getter, js_name = azimuthAngle)]
pub fn azimuth_angle(this: &PointerEventExt) -> f64;
}
let event: &PointerEventExt = event.unchecked_ref();
match kind {
PointerKind::Mouse => PointerSource::Mouse,
PointerKind::Touch(id) => PointerSource::Touch {
finger_id: id,
force: Some(Force::Normalized(event.pressure().into())),
},
PointerKind::TabletTool(tool) => {
let data = TabletToolData {
force: Some(Force::Normalized(event.pressure().into())),
tangential_force: Some(event.tangential_pressure()),
twist: Some(event.twist().try_into().expect("found invalid `twist`")),
tilt: Some(TabletToolTilt {
x: event.tilt_x().try_into().expect("found invalid `tiltX`"),
y: event.tilt_y().try_into().expect("found invalid `tiltY`"),
}),
angle: event
.altitude_angle()
.map(|altitude| TabletToolAngle { altitude, azimuth: event.azimuth_angle() }),
};
PointerSource::TabletTool { kind: tool, data }
},
PointerKind::Unknown => PointerSource::Unknown,
}
}
pub fn pointer_position(event: &MouseEvent) -> LogicalPosition<f64> {
#[wasm_bindgen]
extern "C" {
type MouseEventExt;
@ -113,7 +224,7 @@ impl MouseDelta {
Some(Engine::Chromium) => Self::Chromium,
// Firefox has wrong movement values in coalesced events.
Some(Engine::Gecko) if has_coalesced_events_support(event) => Self::Gecko {
old_position: mouse_position(event),
old_position: pointer_position(event),
old_delta: LogicalPosition::new(
event.movement_x() as f64,
event.movement_y() as f64,
@ -129,7 +240,7 @@ impl MouseDelta {
PhysicalPosition::new(event.movement_x(), event.movement_y()).into()
},
MouseDelta::Gecko { old_position, old_delta } => {
let new_position = mouse_position(event);
let new_position = pointer_position(event);
let x = new_position.x - old_position.x + old_delta.x;
let y = new_position.y - old_position.y + old_delta.y;
*old_position = new_position;
@ -160,14 +271,6 @@ pub fn mouse_scroll_delta(
}
}
pub fn pointer_type(event: &PointerEvent, pointer_id: i32) -> PointerKind {
match event.pointer_type().as_str() {
"mouse" => PointerKind::Mouse,
"touch" => PointerKind::Touch(FingerId::from_raw(pointer_id as usize)),
_ => PointerKind::Unknown,
}
}
pub fn key_code(event: &KeyboardEvent) -> PhysicalKey {
// Use keyboard-types' parsing (it is based on the W3C standard).
match event.code().parse() {

View file

@ -3,14 +3,14 @@ use std::rc::Rc;
use dpi::PhysicalPosition;
use web_sys::PointerEvent;
use winit_core::event::{ButtonSource, DeviceId, ElementState, Force, PointerKind, PointerSource};
use winit_core::event::{ButtonSource, DeviceId, ElementState, PointerKind, PointerSource};
use winit_core::keyboard::ModifiersState;
use super::canvas::Common;
use super::event;
use super::event_handle::EventListenerHandle;
use crate::event::mkdid;
use crate::web_sys::event::mouse_button_to_id;
use crate::web_sys::event::ButtonsState;
#[allow(dead_code)]
pub(super) struct PointerHandler {
@ -46,8 +46,8 @@ impl PointerHandler {
let pointer_id = event.pointer_id();
let device_id = mkdid(pointer_id);
let position =
event::mouse_position(&event).to_physical(super::scale_factor(&window));
let kind = event::pointer_type(&event, pointer_id);
event::pointer_position(&event).to_physical(super::scale_factor(&window));
let kind = event::pointer_kind(&event, pointer_id);
handler(modifiers, device_id, event.is_primary(), position, kind);
}));
}
@ -64,8 +64,8 @@ impl PointerHandler {
let pointer_id = event.pointer_id();
let device_id = mkdid(pointer_id);
let position =
event::mouse_position(&event).to_physical(super::scale_factor(&window));
let kind = event::pointer_type(&event, pointer_id);
event::pointer_position(&event).to_physical(super::scale_factor(&window));
let kind = event::pointer_kind(&event, pointer_id);
handler(modifiers, device_id, event.is_primary(), position, kind);
}));
}
@ -80,24 +80,25 @@ impl PointerHandler {
Some(canvas_common.add_event("pointerup", move |event: PointerEvent| {
let modifiers = event::mouse_modifiers(&event);
let pointer_id = event.pointer_id();
let kind = event::pointer_type(&event, pointer_id);
let kind = event::pointer_kind(&event, pointer_id);
let button = event::raw_button(&event).expect("no button pressed");
let button = event::mouse_button(&event).expect("no mouse button pressed");
let source = match kind {
PointerKind::Mouse => ButtonSource::Mouse(button),
PointerKind::Touch(finger_id) => ButtonSource::Touch {
finger_id,
force: Some(Force::Normalized(event.pressure().into())),
let source = match event::pointer_source(&event, kind) {
PointerSource::Mouse => ButtonSource::Mouse(event::mouse_button(button)),
PointerSource::Touch { finger_id, force } => {
ButtonSource::Touch { finger_id, force }
},
PointerKind::Unknown => ButtonSource::Unknown(mouse_button_to_id(button)),
PointerSource::TabletTool { kind, data } => {
ButtonSource::TabletTool { kind, button: event::tool_button(button), data }
},
PointerSource::Unknown => ButtonSource::Unknown(button),
};
handler(
modifiers,
mkdid(pointer_id),
event.is_primary(),
event::mouse_position(&event).to_physical(super::scale_factor(&window)),
event::pointer_position(&event).to_physical(super::scale_factor(&window)),
source,
)
}));
@ -125,11 +126,11 @@ impl PointerHandler {
let modifiers = event::mouse_modifiers(&event);
let pointer_id = event.pointer_id();
let kind = event::pointer_type(&event, pointer_id);
let button = event::mouse_button(&event).expect("no mouse button pressed");
let kind = event::pointer_kind(&event, pointer_id);
let button = event::raw_button(&event).expect("no button pressed");
let source = match kind {
PointerKind::Mouse => {
let source = match event::pointer_source(&event, kind) {
PointerSource::Mouse => {
// Error is swallowed here since the error would occur every time the
// mouse is clicked when the cursor is
// grabbed, and there is probably not a
@ -137,20 +138,29 @@ impl PointerHandler {
// care if it fails.
let _e = canvas.set_pointer_capture(pointer_id);
ButtonSource::Mouse(button)
ButtonSource::Mouse(event::mouse_button(button))
},
PointerKind::Touch(finger_id) => ButtonSource::Touch {
finger_id,
force: Some(Force::Normalized(event.pressure().into())),
PointerSource::Touch { finger_id, force } => {
ButtonSource::Touch { finger_id, force }
},
PointerKind::Unknown => ButtonSource::Unknown(mouse_button_to_id(button)),
PointerSource::TabletTool { kind, data } => {
// Error is swallowed here since the error would occur every time the
// mouse is clicked when the cursor is
// grabbed, and there is probably not a
// situation where this could fail, that we
// care if it fails.
let _e = canvas.set_pointer_capture(pointer_id);
ButtonSource::TabletTool { kind, button: event::tool_button(button), data }
},
PointerSource::Unknown => ButtonSource::Unknown(button),
};
handler(
modifiers,
mkdid(pointer_id),
event.is_primary(),
event::mouse_position(&event).to_physical(super::scale_factor(&window)),
event::pointer_position(&event).to_physical(super::scale_factor(&window)),
source,
)
}));
@ -186,11 +196,11 @@ impl PointerHandler {
Some(canvas_common.add_event("pointermove", move |event: PointerEvent| {
let pointer_id = event.pointer_id();
let device_id = mkdid(pointer_id);
let kind = event::pointer_type(&event, pointer_id);
let kind = event::pointer_kind(&event, pointer_id);
let primary = event.is_primary();
// chorded button event
if let Some(button) = event::mouse_button(&event) {
if let Some(button) = event::raw_button(&event) {
if prevent_default.get() {
// prevent text selection
event.prevent_default();
@ -198,34 +208,36 @@ impl PointerHandler {
let _ = canvas.focus();
}
let state = if event::mouse_buttons(&event).contains(button.into()) {
let state = if event::pointer_buttons(&event)
.contains(ButtonsState::from_bits_retain(button))
{
ElementState::Pressed
} else {
ElementState::Released
};
let button = match kind {
PointerKind::Mouse => ButtonSource::Mouse(button),
PointerKind::Touch(finger_id) => {
let button_id = mouse_button_to_id(button);
if button_id != 1 {
tracing::error!("unexpected touch button id: {button_id}");
let button = match event::pointer_source(&event, kind) {
PointerSource::Mouse => ButtonSource::Mouse(event::mouse_button(button)),
PointerSource::Touch { finger_id, force } => {
if button != 0 {
tracing::error!("unexpected touch button id: {button}");
}
ButtonSource::Touch {
finger_id,
force: Some(Force::Normalized(event.pressure().into())),
}
ButtonSource::Touch { finger_id, force }
},
PointerKind::Unknown => todo!(),
PointerSource::TabletTool { kind, data } => ButtonSource::TabletTool {
kind,
button: event::tool_button(button),
data,
},
PointerSource::Unknown => ButtonSource::Unknown(button),
};
button_handler(
event::mouse_modifiers(&event),
device_id,
primary,
event::mouse_position(&event).to_physical(super::scale_factor(&window)),
event::pointer_position(&event).to_physical(super::scale_factor(&window)),
state,
button,
);
@ -242,15 +254,8 @@ impl PointerHandler {
(
event::mouse_modifiers(&event),
event.is_primary(),
event::mouse_position(&event).to_physical(scale),
match kind {
PointerKind::Mouse => PointerSource::Mouse,
PointerKind::Touch(finger_id) => PointerSource::Touch {
finger_id,
force: Some(Force::Normalized(event.pressure().into())),
},
PointerKind::Unknown => PointerSource::Unknown,
},
event::pointer_position(&event).to_physical(scale),
event::pointer_source(&event, kind),
)
}),
);