diff --git a/winit-x11/src/event_loop.rs b/winit-x11/src/event_loop.rs index 990be32be7..446aedd3df 100644 --- a/winit-x11/src/event_loop.rs +++ b/winit-x11/src/event_loop.rs @@ -37,13 +37,11 @@ use x11rb::protocol::{xkb, xproto}; use x11rb::x11_utils::X11Error as LogicalError; use x11rb::xcb_ffi::ReplyOrIdError; -use crate::atoms::{ - _NET_WM_PING, _NET_WM_SYNC_REQUEST, ABS_PRESSURE, ABS_TILT_X, ABS_TILT_Y, ABS_X, ABS_Y, Atoms, - WM_DELETE_WINDOW, -}; +use crate::atoms::{_NET_WM_PING, _NET_WM_SYNC_REQUEST, Atoms, WM_DELETE_WINDOW}; use crate::dnd::Dnd; use crate::event_processor::{EventProcessor, MAX_MOD_REPLAY_LEN}; use crate::ime::{self, Ime, ImeCreationError, ImeSender}; +use crate::tablet::TabletDevice; use crate::util::{self, CustomCursor}; use crate::window::{UnownedWindow, Window}; use crate::xdisplay::{XConnection, XError, XNotSupported}; @@ -374,6 +372,7 @@ impl EventLoop { let event_processor = EventProcessor { target: window_target, devices: Default::default(), + active_pointer_sources: Default::default(), randr_event_offset, ime_receiver, ime_event_receiver, @@ -398,7 +397,8 @@ impl EventLoop { .select_xinput_events( root, ALL_DEVICES, - x11rb::protocol::xinput::XIEventMask::HIERARCHY, + x11rb::protocol::xinput::XIEventMask::HIERARCHY + | x11rb::protocol::xinput::XIEventMask::DEVICE_CHANGED, ) .expect_then_ignore_error("Failed to register for XInput2 device hotplug events"); @@ -1099,18 +1099,20 @@ pub(crate) fn mkdid(w: xinput::DeviceId) -> DeviceId { pub struct Device { _name: String, pub(crate) scroll_axes: Vec<(i32, ScrollAxis)>, + pub(crate) master_pointer: bool, + /// Physical devices supplying the current classes of a master pointer. + pub(crate) class_sources: Vec, // For master devices, this is the paired device (pointer <-> keyboard). // For slave devices, this is the master. pub(crate) attachment: c_int, pub(crate) r#type: DeviceType, } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] pub(crate) enum DeviceType { Mouse, Touch, - Pen, - Eraser, + Tablet(TabletDevice), } #[derive(Debug, Copy, Clone)] @@ -1130,11 +1132,23 @@ impl Device { pub(crate) fn new(info: &ffi::XIDeviceInfo, atoms: &Atoms) -> Self { let name = unsafe { CStr::from_ptr(info.name).to_string_lossy() }; let mut scroll_axes = Vec::new(); - let mut r#type = None; + let classes = Device::classes(info); + let mut has_touch_class = false; + let master_pointer = info._use == ffi::XIMasterPointer; + let mut class_sources = Vec::new(); + + if master_pointer { + for &class_ptr in classes { + let source = unsafe { (*class_ptr).sourceid }; + if !class_sources.contains(&source) { + class_sources.push(source); + } + } + } if Device::physical_device(info) { // Identify scroll axes - for &class_ptr in Device::classes(info) { + for &class_ptr in classes { let ty = unsafe { (*class_ptr)._type }; if ty == ffi::XIScrollClass { let info = unsafe { &*(class_ptr as *const ffi::XIScrollClassInfo) }; @@ -1148,32 +1162,30 @@ impl Device { position: 0.0, })); } else if ty == ffi::XITouchClass { - r#type = Some(DeviceType::Touch); - } else if r#type.is_none() && ty == ffi::XIValuatorClass { - let info = unsafe { &*(class_ptr as *const ffi::XIValuatorClassInfo) }; - let atom = info.label as xproto::Atom; - - if atom == atoms[ABS_X] - || atom == atoms[ABS_Y] - || atom == atoms[ABS_PRESSURE] - || atom == atoms[ABS_TILT_X] - || atom == atoms[ABS_TILT_Y] - { - if name.contains("eraser") { - r#type = Some(DeviceType::Eraser); - } else { - r#type = Some(DeviceType::Pen); - } - } + has_touch_class = true; } } } + let r#type = if Device::pointer_source(info) { + if has_touch_class { + DeviceType::Touch + } else if let Some(tablet) = TabletDevice::from_xinput(&name, classes, atoms) { + DeviceType::Tablet(tablet) + } else { + DeviceType::Mouse + } + } else { + DeviceType::Mouse + }; + let mut device = Device { _name: name.into_owned(), scroll_axes, + master_pointer, + class_sources, attachment: info.attachment, - r#type: r#type.unwrap_or(DeviceType::Mouse), + r#type, }; device.reset_scroll_position(info); device @@ -1202,6 +1214,11 @@ impl Device { || info._use == ffi::XIFloatingSlave } + #[inline] + fn pointer_source(info: &ffi::XIDeviceInfo) -> bool { + info._use == ffi::XISlavePointer || info._use == ffi::XIFloatingSlave + } + #[inline] fn classes(info: &ffi::XIDeviceInfo) -> &[*const ffi::XIAnyClassInfo] { unsafe { diff --git a/winit-x11/src/event_processor.rs b/winit-x11/src/event_processor.rs index 425c904a4b..b85f1c1e94 100644 --- a/winit-x11/src/event_processor.rs +++ b/winit-x11/src/event_processor.rs @@ -11,15 +11,15 @@ use winit_common::xkb::{self, Context, XkbState}; use winit_core::application::ApplicationHandler; use winit_core::event::{ ButtonSource, DeviceEvent, DeviceId, ElementState, FingerId, Ime, MouseButton, - MouseScrollDelta, PointerKind, PointerSource, RawKeyEvent, SurfaceSizeWriter, TouchPhase, - WindowEvent, + MouseScrollDelta, PointerKind, PointerSource, RawKeyEvent, SurfaceSizeWriter, TabletToolData, + TabletToolKind, TouchPhase, WindowEvent, }; use winit_core::event_loop::DndAction; use winit_core::keyboard::ModifiersState; use winit_core::window::WindowId; use x11_dl::xinput2::{ - self, XIDeviceEvent, XIEnterEvent, XIFocusInEvent, XIFocusOutEvent, XIHierarchyEvent, - XILeaveEvent, XIModifierState, XIRawEvent, + self, XIDeviceChangedEvent, XIDeviceEvent, XIEnterEvent, XIFocusInEvent, XIFocusOutEvent, + XIHierarchyEvent, XILeaveEvent, XIModifierState, XIRawEvent, }; use x11_dl::xlib::{ self, Display as XDisplay, Window as XWindow, XAnyEvent, XClientMessageEvent, XConfigureEvent, @@ -40,6 +40,7 @@ use crate::event_loop::{ ScrollOrientation, mkdid, mkwid, }; use crate::ime::{ImeEvent, ImeEventReceiver, ImeReceiver, ImeRequest}; +use crate::tablet::{for_each_packed_valuator, tablet_button}; use crate::util; use crate::util::cookie::GenericEventCookie; use crate::window::UnownedWindow; @@ -56,6 +57,9 @@ pub struct EventProcessor { pub ime_event_receiver: ImeEventReceiver, pub randr_event_offset: u8, pub devices: RefCell>, + /// The active physical source for each master pointer, seeded from its classes and updated by + /// pointer and device-change events. + pub active_pointer_sources: RefCell>, pub xi2ext: ExtensionInformation, pub xkbext: ExtensionInformation, pub target: ActiveEventLoop, @@ -277,6 +281,10 @@ impl EventProcessor { let xev: &XIHierarchyEvent = unsafe { xev.as_event() }; self.xinput2_hierarchy_changed(xev); }, + xinput2::XI_DeviceChanged => { + let xev: &XIDeviceChangedEvent = unsafe { xev.as_event() }; + self.xinput2_device_changed(xev); + }, _ => {}, } }, @@ -327,14 +335,120 @@ impl EventProcessor { } pub fn init_device(&self, device: xinput::DeviceId) { - let mut devices = self.devices.borrow_mut(); - if let Some(info) = DeviceInfo::get(&self.target.xconn, device as _) { - let atoms = self.target.x_connection().atoms(); + let mut queried_master_pointer = false; + { + let mut devices = self.devices.borrow_mut(); + if let Some(info) = DeviceInfo::get(&self.target.xconn, device as _) { + let atoms = self.target.x_connection().atoms(); - for info in info.iter() { - devices.insert(mkdid(info.deviceid as xinput::DeviceId), Device::new(info, atoms)); + for info in info.iter() { + let device = Device::new(info, atoms); + queried_master_pointer |= device.master_pointer; + devices.insert(mkdid(info.deviceid as xinput::DeviceId), device); + } } } + + if queried_master_pointer { + self.seed_active_pointer_sources(); + } + } + + fn seed_active_pointer_sources(&self) { + let updates = { + let devices = self.devices.borrow(); + devices + .iter() + .filter(|(_, device)| device.master_pointer) + .map(|(&master, device)| { + let source = find_tablet_class_source(&device.class_sources, |source| { + devices + .get(&source) + .is_some_and(|device| matches!(&device.r#type, DeviceType::Tablet(_))) + }); + (master, source) + }) + .collect::>() + }; + + let mut active_sources = self.active_pointer_sources.borrow_mut(); + for (master, source) in updates { + if let Some(source) = source { + active_sources.insert(master, source); + } else { + active_sources.remove(&master); + } + } + } + + fn record_pointer_source(&self, source: xinput::DeviceId, master: xinput::DeviceId) { + let source_id = mkdid(source); + if !self.devices.borrow().contains_key(&source_id) { + self.init_device(source); + } + + if source != master && self.devices.borrow().contains_key(&source_id) { + self.active_pointer_sources.borrow_mut().insert(mkdid(master), source_id); + } + } + + fn tablet_source( + &self, + source: xinput::DeviceId, + master: xinput::DeviceId, + ) -> Option { + self.record_pointer_source(source, master); + + let source_id = mkdid(source); + if self + .devices + .borrow() + .get(&source_id) + .is_some_and(|device| matches!(&device.r#type, DeviceType::Tablet(_))) + { + return Some(source_id); + } + + // A distinct source ID is the authoritative physical device. If querying it failed or it + // is known to be a non-tablet, do not fall back to a potentially stale master mapping. + if source != master { + return None; + } + + let source_id = self.active_pointer_sources.borrow().get(&mkdid(master)).copied()?; + self.devices + .borrow() + .get(&source_id) + .is_some_and(|device| matches!(&device.r#type, DeviceType::Tablet(_))) + .then_some(source_id) + } + + fn tablet_event_data( + &self, + source: xinput::DeviceId, + master: xinput::DeviceId, + valuators: &xinput2::XIValuatorState, + ) -> Option<(DeviceId, TabletToolKind, TabletToolData)> { + let source_id = self.tablet_source(source, master)?; + let mut devices = self.devices.borrow_mut(); + let DeviceType::Tablet(tablet) = &mut devices.get_mut(&source_id)?.r#type else { + return None; + }; + tablet.update_valuators(valuators); + Some((source_id, tablet.kind, tablet.data())) + } + + fn tablet_kind( + &self, + source: xinput::DeviceId, + master: xinput::DeviceId, + ) -> Option<(DeviceId, TabletToolKind)> { + let source_id = self.tablet_source(source, master)?; + let devices = self.devices.borrow(); + let DeviceType::Tablet(tablet) = &devices.get(&source_id)?.r#type else { + return None; + }; + Some((source_id, tablet.kind)) } pub fn with_window(&self, window_id: xproto::Window, callback: F) -> Option @@ -1080,20 +1194,10 @@ impl EventProcessor { app: &mut dyn ApplicationHandler, ) { let window_id = mkwid(event.event as xproto::Window); - let device_id = Some(mkdid(event.deviceid as xinput::DeviceId)); // Set the timestamp. self.target.xconn.set_timestamp(event.time as xproto::Timestamp); - let Some(DeviceType::Mouse) = self - .devices - .borrow() - .get(&mkdid(event.sourceid as xinput::DeviceId)) - .map(|device| device.r#type) - else { - return; - }; - // Deliver multi-touch events instead of emulated mouse events. if (event.flags & xinput2::XIPointerEmulated) != 0 { return; @@ -1101,6 +1205,36 @@ impl EventProcessor { let position = PhysicalPosition::new(event.event_x, event.event_y); + if let Some((device_id, kind, data)) = self.tablet_event_data( + event.sourceid as xinput::DeviceId, + event.deviceid as xinput::DeviceId, + &event.valuators, + ) { + let Some(button) = tablet_button(event.detail as u32) else { + return; + }; + let event = WindowEvent::PointerButton { + device_id: Some(device_id), + primary: true, + state, + position, + button: ButtonSource::TabletTool { kind, button, data }, + }; + app.window_event(&self.target, window_id, event); + return; + } + + let is_mouse = self + .devices + .borrow() + .get(&mkdid(event.sourceid as xinput::DeviceId)) + .is_some_and(|device| matches!(&device.r#type, DeviceType::Mouse)); + if !is_mouse { + return; + } + + let device_id = Some(mkdid(event.deviceid as xinput::DeviceId)); + let event = match event.detail as u32 { xlib::Button1 => WindowEvent::PointerButton { device_id, @@ -1170,18 +1304,42 @@ impl EventProcessor { // Set the timestamp. self.target.xconn.set_timestamp(event.time as xproto::Timestamp); - let Some(DeviceType::Mouse) = self + let window = event.event as xproto::Window; + let window_id = mkwid(window); + + if let Some((device_id, kind, data)) = self.tablet_event_data( + event.sourceid as xinput::DeviceId, + event.deviceid as xinput::DeviceId, + &event.valuators, + ) { + if !self.window_exists(window) { + return; + } + + let new_cursor_pos = (event.event_x, event.event_y); + self.with_window(window, |window| { + window.shared_state_lock().cursor_pos = Some(new_cursor_pos); + }); + let event = WindowEvent::PointerMoved { + device_id: Some(device_id), + primary: true, + position: PhysicalPosition::new(event.event_x, event.event_y), + source: PointerSource::TabletTool { kind, data }, + }; + app.window_event(&self.target, window_id, event); + return; + } + + let is_mouse = self .devices .borrow() .get(&mkdid(event.sourceid as xinput::DeviceId)) - .map(|device| device.r#type) - else { + .is_some_and(|device| matches!(&device.r#type, DeviceType::Mouse)); + if !is_mouse { return; - }; + } let device_id = Some(mkdid(event.deviceid as xinput::DeviceId)); - let window = event.event as xproto::Window; - let window_id = mkwid(window); let new_cursor_pos = (event.event_x, event.event_y); let cursor_moved = self.with_window(window, |window| { @@ -1204,8 +1362,12 @@ impl EventProcessor { } // More gymnastics, for self.devices - let mask = unsafe { - slice::from_raw_parts(event.valuators.mask, event.valuators.mask_len as usize) + let mask = if event.valuators.mask_len <= 0 { + &[] + } else { + unsafe { + slice::from_raw_parts(event.valuators.mask, event.valuators.mask_len as usize) + } }; let mut devices = self.devices.borrow_mut(); let physical_device = match devices.get_mut(&mkdid(event.sourceid as xinput::DeviceId)) { @@ -1214,32 +1376,28 @@ impl EventProcessor { }; let mut events = Vec::new(); - let mut value = event.valuators.values; - for i in 0..event.valuators.mask_len * 8 { - if !xinput2::XIMaskIsSet(mask, i) { - continue; - } - - let x = unsafe { *value }; - - if let Some(&mut (_, ref mut info)) = - physical_device.scroll_axes.iter_mut().find(|&&mut (axis, _)| axis == i as _) - { - let delta = (x - info.position) / info.increment; - info.position = x; - // X11 vertical scroll coordinates are opposite to winit's - let delta = match info.orientation { - ScrollOrientation::Horizontal => { - MouseScrollDelta::LineDelta(-delta as f32, 0.0) - }, - ScrollOrientation::Vertical => MouseScrollDelta::LineDelta(0.0, -delta as f32), - }; - - let event = WindowEvent::MouseWheel { device_id, delta, phase: TouchPhase::Moved }; - events.push(event); - } + unsafe { + for_each_packed_valuator(mask, event.valuators.values, false, |i, x| { + if let Some(&mut (_, ref mut info)) = + physical_device.scroll_axes.iter_mut().find(|&&mut (axis, _)| axis == i) + { + let delta = (x - info.position) / info.increment; + info.position = x; + // X11 vertical scroll coordinates are opposite to winit's + let delta = match info.orientation { + ScrollOrientation::Horizontal => { + MouseScrollDelta::LineDelta(-delta as f32, 0.0) + }, + ScrollOrientation::Vertical => { + MouseScrollDelta::LineDelta(0.0, -delta as f32) + }, + }; - value = unsafe { value.offset(1) }; + let event = + WindowEvent::MouseWheel { device_id, delta, phase: TouchPhase::Moved }; + events.push(event); + } + }); } for event in events { @@ -1253,7 +1411,8 @@ impl EventProcessor { let window = event.event as xproto::Window; let window_id = mkwid(window); - let device_id = mkdid(event.deviceid as xinput::DeviceId); + let tablet = self + .tablet_kind(event.sourceid as xinput::DeviceId, event.deviceid as xinput::DeviceId); if let Some(all_info) = DeviceInfo::get(&self.target.xconn, ALL_DEVICES.into()) { let mut devices = self.devices.borrow_mut(); @@ -1273,11 +1432,21 @@ impl EventProcessor { } if self.window_exists(window) { - let device_id = Some(device_id); let position = PhysicalPosition::new(event.event_x, event.event_y); + if let Some((device_id, kind)) = tablet { + let event = WindowEvent::PointerEntered { + device_id: Some(device_id), + primary: true, + position, + kind: PointerKind::TabletTool(kind), + }; + app.window_event(&self.target, window_id, event); + return; + } + let event = WindowEvent::PointerEntered { - device_id, + device_id: Some(mkdid(event.deviceid as xinput::DeviceId)), primary: true, position, kind: PointerKind::Mouse, @@ -1296,6 +1465,19 @@ impl EventProcessor { // been destroyed, which the user presumably doesn't want to deal with. if self.window_exists(window) { let window_id = mkwid(window); + if let Some((device_id, kind)) = self + .tablet_kind(event.sourceid as xinput::DeviceId, event.deviceid as xinput::DeviceId) + { + let event = WindowEvent::PointerLeft { + device_id: Some(device_id), + primary: true, + position: Some(PhysicalPosition::new(event.event_x, event.event_y)), + kind: PointerKind::TabletTool(kind), + }; + app.window_event(&self.target, window_id, event); + return; + } + let event = WindowEvent::PointerLeft { device_id: Some(mkdid(event.deviceid as xinput::DeviceId)), primary: true, @@ -1411,6 +1593,10 @@ impl EventProcessor { fn xinput2_touch(&mut self, xev: &XIDeviceEvent, phase: i32, app: &mut dyn ApplicationHandler) { // Set the timestamp. self.target.xconn.set_timestamp(xev.time as xproto::Timestamp); + self.record_pointer_source( + xev.sourceid as xinput::DeviceId, + xev.deviceid as xinput::DeviceId, + ); let window = xev.event as xproto::Window; if self.window_exists(window) { @@ -1492,6 +1678,10 @@ impl EventProcessor { ) { // Set the timestamp. self.target.xconn.set_timestamp(xev.time as xproto::Timestamp); + self.record_pointer_source( + xev.sourceid as xinput::DeviceId, + xev.deviceid as xinput::DeviceId, + ); if xev.flags & xinput2::XIPointerEmulated == 0 { let event = DeviceEvent::Button { state, button: xev.detail as u32 }; @@ -1504,38 +1694,39 @@ impl EventProcessor { self.target.xconn.set_timestamp(xev.time as xproto::Timestamp); let did = Some(mkdid(xev.deviceid as xinput::DeviceId)); - let mask = - unsafe { slice::from_raw_parts(xev.valuators.mask, xev.valuators.mask_len as usize) }; - let mut value = xev.raw_values; + let mask = if xev.valuators.mask_len <= 0 { + &[] + } else { + unsafe { slice::from_raw_parts(xev.valuators.mask, xev.valuators.mask_len as usize) } + }; let mut mouse_delta = util::Delta::default(); let mut scroll_delta = util::Delta::default(); - for i in 0..xev.valuators.mask_len * 8 { - if !xinput2::XIMaskIsSet(mask, i) { - continue; - } - let x = unsafe { value.read_unaligned() }; - - // We assume that every XInput2 device with analog axes is a pointing device emitting - // relative coordinates. - match i { - 0 => mouse_delta.set_x(x), - 1 => mouse_delta.set_y(x), - 2 => scroll_delta.set_x(x as f32), - 3 => scroll_delta.set_y(x as f32), - _ => {}, - } - - value = unsafe { value.offset(1) }; + unsafe { + for_each_packed_valuator(mask, xev.raw_values, true, |i, x| { + // We assume that every XInput2 device with analog axes is a pointing device + // emitting relative coordinates. + match i { + 0 => mouse_delta.set_x(x), + 1 => mouse_delta.set_y(x), + 2 => scroll_delta.set_x(x as f32), + 3 => scroll_delta.set_y(x as f32), + _ => {}, + } + }); } - let Some(DeviceType::Mouse) = self + self.record_pointer_source( + xev.sourceid as xinput::DeviceId, + xev.deviceid as xinput::DeviceId, + ); + let is_mouse = self .devices .borrow() .get(&mkdid(xev.sourceid as xinput::DeviceId)) - .map(|device| device.r#type) - else { + .is_some_and(|device| matches!(&device.r#type, DeviceType::Mouse)); + if !is_mouse { return; - }; + } if let Some(mouse_delta) = mouse_delta.consume() { app.device_event(&self.target, did, DeviceEvent::PointerMotion { delta: mouse_delta }); @@ -1574,15 +1765,47 @@ impl EventProcessor { self.target.xconn.set_timestamp(xev.time as xproto::Timestamp); let infos = unsafe { slice::from_raw_parts(xev.info, xev.num_info as usize) }; for info in infos { - if 0 != info.flags & (xinput2::XISlaveAdded | xinput2::XIMasterAdded) { + if 0 != info.flags + & (xinput2::XISlaveAdded + | xinput2::XIMasterAdded + | xinput2::XISlaveAttached + | xinput2::XIDeviceEnabled) + { self.init_device(info.deviceid as xinput::DeviceId); } else if 0 != info.flags & (xinput2::XISlaveRemoved | xinput2::XIMasterRemoved) { + let removed = mkdid(info.deviceid as xinput::DeviceId); let mut devices = self.devices.borrow_mut(); - devices.remove(&mkdid(info.deviceid as xinput::DeviceId)); + devices.remove(&removed); + drop(devices); + self.active_pointer_sources + .borrow_mut() + .retain(|master, source| *master != removed && *source != removed); } } } + fn xinput2_device_changed(&mut self, xev: &XIDeviceChangedEvent) { + self.target.xconn.set_timestamp(xev.time as xproto::Timestamp); + + let device = xev.deviceid as xinput::DeviceId; + let source = xev.sourceid as xinput::DeviceId; + match xev.reason { + xinput2::XISlaveSwitch => { + self.init_device(source); + if source != device { + self.active_pointer_sources.borrow_mut().insert(mkdid(device), mkdid(source)); + } + }, + xinput2::XIDeviceChange => { + self.init_device(device); + if source != device { + self.init_device(source); + } + }, + _ => {}, + } + } + fn xkb_event(&mut self, xev: &XkbAnyEvent, app: &mut dyn ApplicationHandler) { match xev.xkb_type { xlib::XkbNewKeyboardNotify => { @@ -1902,3 +2125,28 @@ fn is_first_touch(first: &mut Option, num: &mut u32, id: u32, phase: i32) - *first == Some(id) } + +fn find_tablet_class_source( + class_sources: &[c_int], + mut is_tablet: impl FnMut(DeviceId) -> bool, +) -> Option { + class_sources.iter().find_map(|&source| { + let source = xinput::DeviceId::try_from(source).ok().map(mkdid)?; + is_tablet(source).then_some(source) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn finds_active_tablet_from_master_class_sources() { + let tablet = mkdid(12); + assert_eq!( + find_tablet_class_source(&[10, 12, 14], |source| source == tablet), + Some(tablet) + ); + assert_eq!(find_tablet_class_source(&[10, 14], |source| source == tablet), None); + } +} diff --git a/winit-x11/src/lib.rs b/winit-x11/src/lib.rs index 79c9d05e8a..670ae78dd8 100644 --- a/winit-x11/src/lib.rs +++ b/winit-x11/src/lib.rs @@ -21,6 +21,7 @@ mod event_processor; pub mod ffi; mod ime; mod monitor; +mod tablet; mod util; mod window; mod xdisplay; diff --git a/winit-x11/src/tablet.rs b/winit-x11/src/tablet.rs new file mode 100644 index 0000000000..5b45a3a6e7 --- /dev/null +++ b/winit-x11/src/tablet.rs @@ -0,0 +1,364 @@ +use std::slice; + +use winit_core::event::{Force, TabletToolButton, TabletToolData, TabletToolKind, TabletToolTilt}; +use x11_dl::xinput2; +use x11rb::protocol::xproto; + +use crate::atoms::{ABS_PRESSURE, ABS_TILT_X, ABS_TILT_Y, ABS_X, ABS_Y, Atoms}; +use crate::ffi; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum AxisLabel { + X, + Y, + Pressure, + TiltX, + TiltY, + Other, +} + +#[derive(Clone, Debug)] +struct ValuatorAxis { + number: i32, + label: AxisLabel, + absolute: bool, + min: f64, + max: f64, + value: Option, +} + +impl ValuatorAxis { + fn new(number: i32, label: AxisLabel, absolute: bool, min: f64, max: f64, value: f64) -> Self { + Self { number, label, absolute, min, max, value: value.is_finite().then_some(value) } + } + + fn update(&mut self, number: i32, value: f64) { + if self.number == number && value.is_finite() { + self.value = Some(value); + } + } +} + +#[derive(Clone, Debug)] +pub(crate) struct TabletDevice { + pub(crate) kind: TabletToolKind, + pressure: Option, + tilt_x: Option, + tilt_y: Option, +} + +impl TabletDevice { + pub(crate) fn from_xinput( + name: &str, + classes: &[*const ffi::XIAnyClassInfo], + atoms: &Atoms, + ) -> Option { + let mut axes = Vec::new(); + + for &class_ptr in classes { + if unsafe { (*class_ptr)._type } != ffi::XIValuatorClass { + continue; + } + + let info = unsafe { &*(class_ptr as *const ffi::XIValuatorClassInfo) }; + let atom = info.label as xproto::Atom; + let label = if atom == atoms[ABS_X] { + AxisLabel::X + } else if atom == atoms[ABS_Y] { + AxisLabel::Y + } else if atom == atoms[ABS_PRESSURE] { + AxisLabel::Pressure + } else if atom == atoms[ABS_TILT_X] { + AxisLabel::TiltX + } else if atom == atoms[ABS_TILT_Y] { + AxisLabel::TiltY + } else { + AxisLabel::Other + }; + axes.push(ValuatorAxis::new( + info.number, + label, + info.mode == ffi::XIModeAbsolute, + info.min, + info.max, + info.value, + )); + } + + classify_device(name, axes) + } + + pub(crate) fn update_valuators(&mut self, valuators: &xinput2::XIValuatorState) { + if valuators.mask_len <= 0 { + return; + } + let mask = unsafe { slice::from_raw_parts(valuators.mask, valuators.mask_len as usize) }; + + // XI2 packs one value for every set bit. The values are not indexed by axis number. + unsafe { + for_each_packed_valuator(mask, valuators.values, false, |number, value| { + self.update_value(number, value); + }); + } + } + + fn update_value(&mut self, number: i32, value: f64) { + if let Some(pressure) = self.pressure.as_mut() { + pressure.update(number, value); + } + if let Some(tilt_x) = self.tilt_x.as_mut() { + tilt_x.update(number, value); + } + if let Some(tilt_y) = self.tilt_y.as_mut() { + tilt_y.update(number, value); + } + } + + pub(crate) fn data(&self) -> TabletToolData { + let force = self.pressure.as_ref().and_then(|axis| { + let value = axis.value?; + let range = axis.max - axis.min; + (range.is_finite() && range > 0.0) + .then(|| Force::Normalized(((value - axis.min) / range).clamp(0.0, 1.0))) + }); + + let tilt_x = self.tilt_x.as_ref().and_then(|axis| axis.value).map(normalize_tilt); + let tilt_y = self.tilt_y.as_ref().and_then(|axis| axis.value).map(normalize_tilt); + let tilt = (tilt_x.is_some() || tilt_y.is_some()) + .then(|| TabletToolTilt { x: tilt_x.unwrap_or(0), y: tilt_y.unwrap_or(0) }); + + TabletToolData { force, tangential_force: None, twist: None, tilt, angle: None } + } +} + +fn classify_device(name: &str, axes: Vec) -> Option { + let lower_name = name.to_ascii_lowercase(); + let strong_name = name_word(&lower_name, "stylus") + || name_word(&lower_name, "pen") + || name_word(&lower_name, "eraser") + || name_word(&lower_name, "brush") + || name_word(&lower_name, "pencil") + || name_word(&lower_name, "airbrush") + || name_word(&lower_name, "finger") + || name_word(&lower_name, "mouse") + || name_word(&lower_name, "cursor") + || name_word(&lower_name, "puck") + || name_word(&lower_name, "lens"); + + let labelled_x = axes.iter().any(|axis| axis.label == AxisLabel::X && axis.absolute); + let labelled_y = axes.iter().any(|axis| axis.label == AxisLabel::Y && axis.absolute); + let fallback_x = axes.iter().any(|axis| axis.number == 0 && axis.absolute); + let fallback_y = axes.iter().any(|axis| axis.number == 1 && axis.absolute); + let has_position = (labelled_x && labelled_y) || (strong_name && fallback_x && fallback_y); + + let pressure = axes.iter().find(|axis| axis.label == AxisLabel::Pressure).cloned(); + let tilt_x = axes.iter().find(|axis| axis.label == AxisLabel::TiltX).cloned(); + let tilt_y = axes.iter().find(|axis| axis.label == AxisLabel::TiltY).cloned(); + let has_tablet_axes = pressure.is_some() || tilt_x.is_some() || tilt_y.is_some(); + + if !has_position || (!has_tablet_axes && !strong_name) { + return None; + } + + let kind = if name_word(&lower_name, "eraser") { + TabletToolKind::Eraser + } else if name_word(&lower_name, "brush") { + TabletToolKind::Brush + } else if name_word(&lower_name, "pencil") { + TabletToolKind::Pencil + } else if name_word(&lower_name, "airbrush") { + TabletToolKind::Airbrush + } else if name_word(&lower_name, "finger") { + TabletToolKind::Finger + } else if name_word(&lower_name, "lens") { + TabletToolKind::Lens + } else if name_word(&lower_name, "mouse") + || name_word(&lower_name, "cursor") + || name_word(&lower_name, "puck") + { + TabletToolKind::Mouse + } else { + TabletToolKind::Pen + }; + + Some(TabletDevice { kind, pressure, tilt_x, tilt_y }) +} + +fn name_word(name: &str, word: &str) -> bool { + name.split(|character: char| !character.is_ascii_alphanumeric()).any(|part| part == word) +} + +fn normalize_tilt(value: f64) -> i8 { + value.round().clamp(-90.0, 90.0) as i8 +} + +pub(crate) fn tablet_button(detail: u32) -> Option { + Some(match detail { + 1 => TabletToolButton::Contact, + 3 => TabletToolButton::Barrel, + 2 => TabletToolButton::Other(1), + 8 => TabletToolButton::Other(3), + 9 => TabletToolButton::Other(4), + detail if detail > 0 && detail <= u16::MAX as u32 => TabletToolButton::Other(detail as u16), + _ => return None, + }) +} + +/// Visits XI2's packed valuator values in axis-number order. +/// +/// # Safety +/// +/// `values` must point to at least as many readable `f64`s as there are set bits in `mask`. +pub(crate) unsafe fn for_each_packed_valuator( + mask: &[u8], + mut values: *const f64, + unaligned: bool, + mut visitor: impl FnMut(i32, f64), +) { + for number in 0..mask.len() * 8 { + if mask[number / 8] & (1 << (number % 8)) == 0 { + continue; + } + + let value = + if unaligned { unsafe { values.read_unaligned() } } else { unsafe { values.read() } }; + visitor(number as i32, value); + values = unsafe { values.add(1) }; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn axis(number: i32, label: AxisLabel, value: f64) -> ValuatorAxis { + ValuatorAxis::new(number, label, true, 0.0, 100.0, value) + } + + #[test] + fn absolute_xy_alone_is_not_a_tablet() { + let axes = vec![axis(0, AxisLabel::X, 0.0), axis(1, AxisLabel::Y, 0.0)]; + assert!(classify_device("Generic absolute pointer", axes).is_none()); + } + + #[test] + fn labels_and_pressure_classify_a_pen_without_name_heuristics() { + let axes = vec![ + axis(0, AxisLabel::X, 0.0), + axis(1, AxisLabel::Y, 0.0), + axis(5, AxisLabel::Pressure, 25.0), + ]; + let tablet = classify_device("Unknown device", axes).unwrap(); + assert_eq!(tablet.kind, TabletToolKind::Pen); + assert_eq!(tablet.data().force, Some(Force::Normalized(0.25))); + } + + #[test] + fn strong_names_choose_supported_tool_kinds() { + let axes = vec![axis(0, AxisLabel::Other, 0.0), axis(1, AxisLabel::Other, 0.0)]; + for (name, expected) in [ + ("Wacom Eraser", TabletToolKind::Eraser), + ("Wacom Brush", TabletToolKind::Brush), + ("Wacom Pencil", TabletToolKind::Pencil), + ("Wacom Airbrush", TabletToolKind::Airbrush), + ("Wacom Finger", TabletToolKind::Finger), + ("Wacom Mouse", TabletToolKind::Mouse), + ("Tablet Cursor Puck", TabletToolKind::Mouse), + ("Tablet Lens", TabletToolKind::Lens), + ] { + assert_eq!(classify_device(name, axes.clone()).unwrap().kind, expected); + } + } + + #[test] + fn packed_valuators_consume_only_set_bits() { + let values = [12.0, 34.0, 56.0]; + let mut visited = Vec::new(); + unsafe { + for_each_packed_valuator(&[0b0010_0101], values.as_ptr(), false, |axis, value| { + visited.push((axis, value)); + }); + } + assert_eq!(visited, [(0, 12.0), (2, 34.0), (5, 56.0)]); + } + + #[test] + fn packed_valuators_handle_empty_and_multiple_mask_bytes() { + let mut empty = Vec::new(); + unsafe { + for_each_packed_valuator(&[], std::ptr::null(), false, |axis, value| { + empty.push((axis, value)); + }); + } + assert!(empty.is_empty()); + + let values = [1.0, 2.0]; + let mut visited = Vec::new(); + unsafe { + for_each_packed_valuator( + &[0b1000_0000, 0b0000_0010], + values.as_ptr(), + false, + |a, v| { + visited.push((a, v)); + }, + ); + } + assert_eq!(visited, [(7, 1.0), (9, 2.0)]); + } + + #[test] + fn tablet_data_normalizes_pressure_and_clamps_tilt() { + let tablet = TabletDevice { + kind: TabletToolKind::Pen, + pressure: Some(ValuatorAxis::new(2, AxisLabel::Pressure, true, 10.0, 20.0, 25.0)), + tilt_x: Some(ValuatorAxis::new(3, AxisLabel::TiltX, true, -64.0, 63.0, 91.0)), + tilt_y: None, + }; + let data = tablet.data(); + assert_eq!(data.force, Some(Force::Normalized(1.0))); + assert_eq!(data.tilt, Some(TabletToolTilt { x: 90, y: 0 })); + } + + #[test] + fn invalid_pressure_range_and_non_finite_tilt_are_absent() { + let tablet = TabletDevice { + kind: TabletToolKind::Pen, + pressure: Some(ValuatorAxis::new(2, AxisLabel::Pressure, true, 1.0, 1.0, 1.0)), + tilt_x: Some(ValuatorAxis::new(3, AxisLabel::TiltX, true, -90.0, 90.0, f64::NAN)), + tilt_y: None, + }; + let data = tablet.data(); + assert_eq!(data.force, None); + assert_eq!(data.tilt, None); + } + + #[test] + fn sparse_updates_retain_absent_axes_and_ignore_non_finite_values() { + let mut tablet = TabletDevice { + kind: TabletToolKind::Pen, + pressure: Some(ValuatorAxis::new(2, AxisLabel::Pressure, true, 0.0, 100.0, 40.0)), + tilt_x: Some(ValuatorAxis::new(3, AxisLabel::TiltX, true, -90.0, 90.0, 10.0)), + tilt_y: None, + }; + + tablet.update_value(3, 20.0); + assert_eq!(tablet.data().force, Some(Force::Normalized(0.4))); + assert_eq!(tablet.data().tilt, Some(TabletToolTilt { x: 20, y: 0 })); + + tablet.update_value(2, f64::NAN); + assert_eq!(tablet.data().force, Some(Force::Normalized(0.4))); + } + + #[test] + fn maps_x_buttons_to_tablet_buttons() { + assert_eq!(tablet_button(1), Some(TabletToolButton::Contact)); + assert_eq!(tablet_button(3), Some(TabletToolButton::Barrel)); + assert_eq!(tablet_button(2), Some(TabletToolButton::Other(1))); + assert_eq!(tablet_button(8), Some(TabletToolButton::Other(3))); + assert_eq!(tablet_button(9), Some(TabletToolButton::Other(4))); + assert_eq!(tablet_button(42), Some(TabletToolButton::Other(42))); + assert_eq!(tablet_button(0), None); + assert_eq!(tablet_button(u16::MAX as u32 + 1), None); + } +} diff --git a/winit/examples/application.rs b/winit/examples/application.rs index 33c942e916..f475fe49ad 100644 --- a/winit/examples/application.rs +++ b/winit/examples/application.rs @@ -474,8 +474,11 @@ impl ApplicationHandler for Application { } } }, - WindowEvent::PointerButton { button, state, .. } => { - info!("Pointer button {button:?} {state:?}"); + WindowEvent::PointerButton { device_id, primary, state, position, button } => { + info!( + "Pointer button device={device_id:?} primary={primary} position={position:?} \ + button={button:?} state={state:?}" + ); let mods = window.modifiers; if let Some(action) = state .is_pressed() @@ -486,12 +489,24 @@ impl ApplicationHandler for Application { self.handle_action_with_window(event_loop, window_id, action); } }, - WindowEvent::PointerLeft { .. } => { - info!("Pointer left Window={window_id:?}"); + WindowEvent::PointerEntered { device_id, primary, position, kind } => { + info!( + "Pointer entered Window={window_id:?} device={device_id:?} primary={primary} \ + position={position:?} kind={kind:?}" + ); + }, + WindowEvent::PointerLeft { device_id, primary, position, kind } => { + info!( + "Pointer left Window={window_id:?} device={device_id:?} primary={primary} \ + position={position:?} kind={kind:?}" + ); window.cursor_left(); }, - WindowEvent::PointerMoved { position, .. } => { - info!("Moved pointer to {position:?}"); + WindowEvent::PointerMoved { device_id, primary, position, source } => { + info!( + "Moved pointer device={device_id:?} primary={primary} position={position:?} \ + source={source:?}" + ); window.cursor_moved(position); }, WindowEvent::ActivationTokenDone { token: _token, .. } => { diff --git a/winit/src/changelog/unreleased.md b/winit/src/changelog/unreleased.md index a14ab1fa72..95ffa2089d 100644 --- a/winit/src/changelog/unreleased.md +++ b/winit/src/changelog/unreleased.md @@ -50,6 +50,8 @@ changelog entry. - On Android, added scancode conversions for more obscure key codes. - On Wayland, added `HoldGesture` event for multi-finger hold gestures - On Wayland, added ext-background-effect-v1 support. +- On X11, add tablet tool support through the unified pointer events, including pressure, tilt, + tool kind, and tablet buttons. ### Changed