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

@ -44,28 +44,29 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{
GetMenu, LoadCursorW, MsgWaitForMultipleObjectsEx, PeekMessageW, PostMessageW,
RegisterClassExW, RegisterWindowMessageA, SetCursor, SetWindowPos, SystemParametersInfoW,
TranslateMessage, CREATESTRUCTW, GWL_STYLE, GWL_USERDATA, HTCAPTION, HTCLIENT, MINMAXINFO,
MNC_CLOSE, MSG, MWMO_INPUTAVAILABLE, NCCALCSIZE_PARAMS, PM_REMOVE, PT_TOUCH, QS_ALLINPUT,
RI_MOUSE_HWHEEL, RI_MOUSE_WHEEL, SC_MINIMIZE, SC_RESTORE, SIZE_MAXIMIZED,
SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE,
SWP_NOZORDER, WHEEL_DELTA, WINDOWPOS, WMSZ_BOTTOM, WMSZ_BOTTOMLEFT, WMSZ_BOTTOMRIGHT,
WMSZ_LEFT, WMSZ_RIGHT, WMSZ_TOP, WMSZ_TOPLEFT, WMSZ_TOPRIGHT, WM_CAPTURECHANGED, WM_CLOSE,
WM_CREATE, WM_DESTROY, WM_DPICHANGED, WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE, WM_GETMINMAXINFO,
WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION, WM_IME_SETCONTEXT, WM_IME_STARTCOMPOSITION,
WM_INPUT, WM_KEYDOWN, WM_KEYUP, WM_KILLFOCUS, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN,
WM_MBUTTONUP, WM_MENUCHAR, WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCACTIVATE,
WM_NCCALCSIZE, WM_NCCREATE, WM_NCDESTROY, WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN,
WM_POINTERUP, WM_POINTERUPDATE, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS,
WM_SETTINGCHANGE, WM_SIZE, WM_SIZING, WM_SYSCOMMAND, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_TOUCH,
WM_WINDOWPOSCHANGED, WM_WINDOWPOSCHANGING, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSEXW,
WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP,
WS_VISIBLE,
MNC_CLOSE, MSG, MWMO_INPUTAVAILABLE, NCCALCSIZE_PARAMS, PEN_FLAG_BARREL, PEN_FLAG_ERASER,
PEN_MASK_PRESSURE, PEN_MASK_ROTATION, PEN_MASK_TILT_X, PEN_MASK_TILT_Y, PM_REMOVE, PT_PEN,
PT_TOUCH, QS_ALLINPUT, RI_MOUSE_HWHEEL, RI_MOUSE_WHEEL, SC_MINIMIZE, SC_RESTORE,
SIZE_MAXIMIZED, SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES, SWP_NOACTIVATE, SWP_NOMOVE,
SWP_NOSIZE, SWP_NOZORDER, WHEEL_DELTA, WINDOWPOS, WMSZ_BOTTOM, WMSZ_BOTTOMLEFT,
WMSZ_BOTTOMRIGHT, WMSZ_LEFT, WMSZ_RIGHT, WMSZ_TOP, WMSZ_TOPLEFT, WMSZ_TOPRIGHT,
WM_CAPTURECHANGED, WM_CLOSE, WM_CREATE, WM_DESTROY, WM_DPICHANGED, WM_ENTERSIZEMOVE,
WM_EXITSIZEMOVE, WM_GETMINMAXINFO, WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION,
WM_IME_SETCONTEXT, WM_IME_STARTCOMPOSITION, WM_INPUT, WM_KEYDOWN, WM_KEYUP, WM_KILLFOCUS,
WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MENUCHAR, WM_MOUSEHWHEEL,
WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCACTIVATE, WM_NCCALCSIZE, WM_NCCREATE, WM_NCDESTROY,
WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN, WM_POINTERUP, WM_POINTERUPDATE, WM_RBUTTONDOWN,
WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS, WM_SETTINGCHANGE, WM_SIZE, WM_SIZING, WM_SYSCOMMAND,
WM_SYSKEYDOWN, WM_SYSKEYUP, WM_TOUCH, WM_WINDOWPOSCHANGED, WM_WINDOWPOSCHANGING,
WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSEXW, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW,
WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, WS_VISIBLE,
};
use winit_core::application::ApplicationHandler;
use winit_core::cursor::{CustomCursor, CustomCursorSource};
use winit_core::error::{EventLoopError, NotSupportedError, RequestError};
use winit_core::event::{
DeviceEvent, DeviceId, FingerId, Force, Ime, RawKeyEvent, SurfaceSizeWriter, TouchPhase,
WindowEvent,
DeviceEvent, DeviceId, FingerId, Force, Ime, RawKeyEvent, SurfaceSizeWriter, TabletToolButton,
TabletToolData, TabletToolKind, TabletToolTilt, TouchPhase, WindowEvent,
};
use winit_core::event_loop::pump_events::PumpStatus;
use winit_core::event_loop::{
@ -106,6 +107,7 @@ pub(crate) struct WindowData {
pub key_event_builder: KeyEventBuilder,
pub _file_drop_handler: Option<FileDropHandler>,
pub userdata_removed: Cell<bool>,
pub last_tablet_down_button_state: Cell<u32>,
pub recurse_depth: Cell<u32>,
}
@ -2085,22 +2087,6 @@ unsafe fn public_window_callback_inner(
continue;
}
let force = if let PT_TOUCH = pointer_info.pointerType {
let mut touch_info = mem::MaybeUninit::uninit();
util::GET_POINTER_TOUCH_INFO.and_then(|GetPointerTouchInfo| {
match unsafe {
GetPointerTouchInfo(pointer_info.pointerId, touch_info.as_mut_ptr())
} {
0 => None,
_ => normalize_pointer_pressure(unsafe {
touch_info.assume_init().pressure
}),
}
})
} else {
None
};
let x = location.x as f64 + x.fract();
let y = location.y as f64 + y.fract();
let position = PhysicalPosition::new(x, y);
@ -2108,60 +2094,83 @@ unsafe fn public_window_callback_inner(
let finger_id = FingerId::from_raw(pointer_info.pointerId as usize);
let primary = util::has_flag(pointer_info.pointerFlags, POINTER_FLAG_PRIMARY);
if util::has_flag(pointer_info.pointerFlags, POINTER_FLAG_DOWN) {
userdata.send_window_event(window, WindowEvent::PointerEntered {
device_id: None,
primary,
position,
kind: if let PT_TOUCH = pointer_info.pointerType {
PointerKind::Touch(finger_id)
} else {
PointerKind::Unknown
let is_down = util::has_flag(pointer_info.pointerFlags, POINTER_FLAG_DOWN);
if is_down || util::has_flag(pointer_info.pointerFlags, POINTER_FLAG_UP) {
let (kind, button) = match pointer_info.pointerType {
PT_TOUCH => (PointerKind::Touch(finger_id), ButtonSource::Touch {
finger_id,
force: force_for_touch(pointer_info.pointerId),
}),
PT_PEN => {
let kind = PointerKind::TabletTool(TabletToolKind::Pen);
let (mut pen_flags, data) =
tablet_tool_info_for_pen(pointer_info.pointerId);
let old_pen_flags =
userdata.last_tablet_down_button_state.replace(pen_flags);
// For release, use a diff.
if !is_down {
pen_flags ^= old_pen_flags;
};
let button = ButtonSource::TabletTool {
kind: TabletToolKind::Pen,
button: pen_flags_to_button(pen_flags),
data,
};
(kind, button)
},
});
userdata.send_window_event(window, WindowEvent::PointerButton {
device_id: None,
primary,
state: Pressed,
position,
button: if let PT_TOUCH = pointer_info.pointerType {
ButtonSource::Touch { finger_id, force }
} else {
ButtonSource::Unknown(0)
},
});
} else if util::has_flag(pointer_info.pointerFlags, POINTER_FLAG_UP) {
userdata.send_window_event(window, WindowEvent::PointerButton {
device_id: None,
primary,
state: Released,
position,
button: if let PT_TOUCH = pointer_info.pointerType {
ButtonSource::Touch { finger_id, force }
} else {
ButtonSource::Unknown(0)
},
});
userdata.send_window_event(window, WindowEvent::PointerLeft {
device_id: None,
primary,
position: Some(position),
kind: if let PT_TOUCH = pointer_info.pointerType {
PointerKind::Touch(finger_id)
} else {
PointerKind::Unknown
},
});
_ => (PointerKind::Unknown, ButtonSource::Unknown(0)),
};
if is_down {
userdata.send_window_event(window, WindowEvent::PointerEntered {
device_id: None,
primary,
position,
kind,
});
userdata.send_window_event(window, WindowEvent::PointerButton {
device_id: None,
primary,
state: Pressed,
position,
button,
});
} else {
userdata.send_window_event(window, WindowEvent::PointerButton {
device_id: None,
primary,
state: Released,
position,
button,
});
userdata.send_window_event(window, WindowEvent::PointerLeft {
device_id: None,
primary,
position: Some(position),
kind,
});
}
} else if util::has_flag(pointer_info.pointerFlags, POINTER_FLAG_UPDATE) {
let source = match pointer_info.pointerType {
PT_TOUCH => PointerSource::Touch {
finger_id,
force: force_for_touch(pointer_info.pointerId),
},
PT_PEN => PointerSource::TabletTool {
kind: TabletToolKind::Pen,
data: tablet_tool_info_for_pen(pointer_info.pointerId).1,
},
_ => PointerSource::Unknown,
};
userdata.send_window_event(window, WindowEvent::PointerMoved {
device_id: None,
primary,
position,
source: if let PT_TOUCH = pointer_info.pointerType {
PointerSource::Touch { finger_id, force }
} else {
PointerSource::Unknown
},
source,
});
} else {
continue;
@ -2691,3 +2700,57 @@ fn apply_win10_dpi_adjustment(
conservative_rect
}
fn force_for_touch(pointer_id: u32) -> Option<Force> {
let mut touch_info = mem::MaybeUninit::uninit();
util::GET_POINTER_TOUCH_INFO.and_then(|GetPointerTouchInfo| {
match unsafe { GetPointerTouchInfo(pointer_id, touch_info.as_mut_ptr()) } {
0 => None,
_ => normalize_pointer_pressure(unsafe { touch_info.assume_init().pressure }),
}
})
}
// Information is stored on the same thing, so we don't save anything by
// splitting the functions.
fn tablet_tool_info_for_pen(pointer_id: u32) -> (u32, TabletToolData) {
let mut tool_data = TabletToolData::default();
let mut tool_button = 0;
let mut tablet_info = mem::MaybeUninit::uninit();
util::GET_POINTER_PEN_INFO.map(|GetPointerPenInfo| {
if unsafe { GetPointerPenInfo(pointer_id, tablet_info.as_mut_ptr()) } == 0 {
return;
}
let pen_info = unsafe { tablet_info.assume_init() };
if pen_info.penMask & PEN_MASK_PRESSURE != 0 {
tool_data.force = normalize_pointer_pressure(pen_info.pressure);
}
if pen_info.penMask & (PEN_MASK_TILT_X | PEN_MASK_TILT_Y) != 0 {
tool_data.tilt =
Some(TabletToolTilt { x: pen_info.tiltX as i8, y: pen_info.tiltY as i8 });
}
if pen_info.penMask & PEN_MASK_ROTATION != 0 {
tool_data.twist = Some(pen_info.rotation as u16);
}
tool_button = pen_info.penFlags;
});
(tool_button, tool_data)
}
// NOTE: According to firefox, the buttons while can be combined, in
// reality they are not.
fn pen_flags_to_button(flags: u32) -> TabletToolButton {
if flags & PEN_FLAG_BARREL != 0 {
TabletToolButton::Barrel
} else if flags & PEN_FLAG_ERASER != 0 {
// NOTE: this is likely not a correct thing to do, but route
// it that way for now.
TabletToolButton::Other(PEN_FLAG_ERASER as u16)
} else {
TabletToolButton::Contact
}
}

View file

@ -16,7 +16,7 @@ use windows_sys::Win32::UI::HiDpi::{
DPI_AWARENESS_CONTEXT, MONITOR_DPI_TYPE, PROCESS_DPI_AWARENESS,
};
use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetActiveWindow;
use windows_sys::Win32::UI::Input::Pointer::{POINTER_INFO, POINTER_TOUCH_INFO};
use windows_sys::Win32::UI::Input::Pointer::{POINTER_INFO, POINTER_PEN_INFO, POINTER_TOUCH_INFO};
use windows_sys::Win32::UI::WindowsAndMessaging::{
ClipCursor, GetClientRect, GetClipCursor, GetCursorPos, GetSystemMetrics, GetWindowPlacement,
GetWindowRect, IsIconic, ShowCursor, IDC_APPSTARTING, IDC_ARROW, IDC_CROSS, IDC_HAND, IDC_HELP,
@ -254,6 +254,8 @@ pub type GetPointerDeviceRects = unsafe extern "system" fn(
pub type GetPointerTouchInfo =
unsafe extern "system" fn(pointer_id: u32, touch_info: *mut POINTER_TOUCH_INFO) -> BOOL;
pub type GetPointerPenInfo =
unsafe extern "system" fn(pointer_id: u32, pen_info: *mut POINTER_PEN_INFO) -> BOOL;
pub(crate) static WIN10_BUILD_VERSION: LazyLock<Option<u32>> = LazyLock::new(|| {
type RtlGetVersion = unsafe extern "system" fn(*mut OSVERSIONINFOW) -> NTSTATUS;
@ -306,6 +308,8 @@ pub(crate) static GET_POINTER_DEVICE_RECTS: LazyLock<Option<GetPointerDeviceRect
LazyLock::new(|| get_function!("user32.dll", GetPointerDeviceRects));
pub(crate) static GET_POINTER_TOUCH_INFO: LazyLock<Option<GetPointerTouchInfo>> =
LazyLock::new(|| get_function!("user32.dll", GetPointerTouchInfo));
pub(crate) static GET_POINTER_PEN_INFO: LazyLock<Option<GetPointerPenInfo>> =
LazyLock::new(|| get_function!("user32.dll", GetPointerPenInfo));
pub(crate) fn wrap_device_id(id: u32) -> DeviceId {
DeviceId::from_raw(id as i64)

View file

@ -1263,6 +1263,7 @@ impl InitData<'_> {
_file_drop_handler: file_drop_handler,
userdata_removed: Cell::new(false),
recurse_depth: Cell::new(0),
last_tablet_down_button_state: Cell::new(0),
}
}