From 40855dd9cf72844e8ef509be02ae5e510b1b1013 Mon Sep 17 00:00:00 2001 From: Aspen Pratt Date: Mon, 20 May 2024 16:05:25 -0700 Subject: [PATCH 01/15] prepare deps, features, and examples for xlib support --- Cargo.lock | 1 + Cargo.toml | 8 ++++++-- build.rs | 2 ++ examples/sdl2.rs | 21 +++++++++++++++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9542162..ea0fc7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2035,6 +2035,7 @@ dependencies = [ "wayland-protocols", "windows 0.54.0", "winit", + "x11-dl", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fda0844..1d57241 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,12 +42,14 @@ strum = { version = "0.26.2", features = ["derive"] } thiserror = "1.0.58" smallvec = "1.13.1" -# Wayland `tablet_unstable_v2` deps. # Crazy `cfg` stolen verbatim from winit's Cargo.toml as I assume they have more wisdom than I [target.'cfg(any(docsrs, all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos")))))'.dependencies] +# Wayland `tablet_unstable_v2` deps. wayland-backend = { version = "0.3.3", features = ["client_system"], optional = true } wayland-client = { version = "0.31.2", optional = true } wayland-protocols = { version = "0.31.2", features = ["client", "unstable"], optional = true } +# Xorg deps. +x11-dl = { version = "2.21.0", optional = true } # Windows Ink `RealTimeStylus` [target.'cfg(any(docsrs, target_os = "windows"))'.dependencies.windows] @@ -62,12 +64,14 @@ features = [ ] [features] -default = ["wayland-tablet-unstable-v2", "windows-ink"] +default = ["wayland-tablet-unstable-v2", "xorg-xinput2", "windows-ink"] # Wayland `tablet_unstable_v2` support # Note: "unstable" here refers to the protocol itself, not to the stability of it's integration into this crate! wayland-tablet-unstable-v2 = ["dep:wayland-backend", "dep:wayland-client", "dep:wayland-protocols"] +xorg-xinput2 = ["dep:x11-dl"] + # Windows Ink `RealTimeStylus` support windows-ink = ["dep:windows"] diff --git a/build.rs b/build.rs index a680cc9..1ae54e3 100644 --- a/build.rs +++ b/build.rs @@ -13,6 +13,8 @@ fn main() { // Wayland tablet is requested and available. Adapted from winit. // lonngg cfg = The feature is on, and (docs or (supported platform and not unsupported platform)) wl_tablet: { all(feature = "wayland-tablet-unstable-v2", any(docsrs, all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))) }, + // Same as above but for xlib `xinput2` support. + xinput2: { all(feature = "xorg-xinput2", any(docsrs, all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))) }, // Ink RealTimeStylus is requested and available ink_rts: { all(feature = "windows-ink", any(docsrs, target_os = "windows")) }, } diff --git a/examples/sdl2.rs b/examples/sdl2.rs index 417a650..af09dbc 100644 --- a/examples/sdl2.rs +++ b/examples/sdl2.rs @@ -24,6 +24,16 @@ mod rwh_bridge { std::ptr::NonNull::new(display).expect("null wayland handle"), ) .into(), + // Xlib... + rwh_05::RawDisplayHandle::Xlib(rwh_05::XlibDisplayHandle { + display, + screen, + .. + }) => raw_window_handle::XlibDisplayHandle::new( + std::ptr::NonNull::new(display), + screen, + ) + .into(), // Windows 32... Has no display handle! rwh_05::RawDisplayHandle::Windows(_) => { raw_window_handle::WindowsDisplayHandle::new().into() @@ -49,6 +59,17 @@ mod rwh_bridge { std::ptr::NonNull::new(surface).expect("null wayland handle"), ) .into(), + // Xlib... + rwh_05::RawWindowHandle::Xlib(rwh_05::XlibWindowHandle { + window, + visual_id, + .. + }) => { + let mut rwh = raw_window_handle::XlibWindowHandle::new(window); + rwh.visual_id = visual_id; + rwh + } + .into(), // Windows 32... rwh_05::RawWindowHandle::Win32(rwh_05::Win32WindowHandle { hinstance, From 5c9d3a00053518aae9a109100fd4b481391d8ae9 Mon Sep 17 00:00:00 2001 From: Aspen Pratt Date: Mon, 20 May 2024 17:09:54 -0700 Subject: [PATCH 02/15] Wire up Xorg placeholder backend to frontend. --- CHANGELOG.md | 3 +++ src/builder.rs | 24 +++++++++++++++++++ src/lib.rs | 4 ++++ src/platform/mod.rs | 48 ++++++++++++++++++++++++++++++++++++- src/platform/xinput2/mod.rs | 31 ++++++++++++++++++++++++ 5 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md create mode 100644 src/platform/xinput2/mod.rs diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..959f775 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# (Unreleased) +* New variant `Backend::XorgXinput2` +* New default feature `xorg-xinput2` diff --git a/src/builder.rs b/src/builder.rs index d47b996..1b8340c 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -118,6 +118,30 @@ impl Builder { }, )) } + #[cfg(xinput2)] + raw_window_handle::RawDisplayHandle::Xlib(_) + | raw_window_handle::RawDisplayHandle::Xcb(_) => { + // We don't actually care about the dispaly handle for xlib! We need the *window* id instead. + // (We manage our own connection and snoop the window events from there.) + // As such, we accept both Xlib and Xcb, since we only care about the numeric window ID which is *server* defined, + // not client library defined. + let window = match rwh.window_handle()?.as_raw() { + raw_window_handle::RawWindowHandle::Xlib( + raw_window_handle::XlibWindowHandle { window, .. }, + ) => window, + raw_window_handle::RawWindowHandle::Xcb( + raw_window_handle::XcbWindowHandle { window, .. }, + ) => u64::from(window.get()), + // The display handle said it was one of these!! + _ => unreachable!(), + }; + + Ok(crate::platform::PlatformManager::XInput2( + // Safety: forwarded to this fn's contract. + // Fixme: unwrap. + unsafe { crate::platform::xinput2::Manager::build_window(self, window) }, + )) + } #[cfg(ink_rts)] raw_window_handle::RawDisplayHandle::Windows(_) => { // We need the window handle for this :V diff --git a/src/lib.rs b/src/lib.rs index 3069f51..51dee7d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,6 +76,8 @@ pub enum Backend { /// /// **Note**: "unstable" here refers to the protocol itself, not to the stability of its integration into this crate! WaylandTabletUnstableV2, + /// [`XInput2`](https://www.x.org/releases/X11R7.7/doc/inputproto/XI2proto.txt) + XorgXInput2, /// [`RealTimeStylus`](https://learn.microsoft.com/en-us/windows/win32/tablet/realtimestylus-reference) /// /// The use of this interface avoids some common problems with the use of Windows Ink in drawing applications, @@ -124,6 +126,8 @@ impl Manager { match self.internal { #[cfg(wl_tablet)] platform::PlatformManager::Wayland(_) => Backend::WaylandTabletUnstableV2, + #[cfg(xinput2)] + platform::PlatformManager::XInput2(_) => Backend::XorgXInput2, #[cfg(ink_rts)] platform::PlatformManager::Ink(_) => Backend::WindowsInkRealTimeStylus, } diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 39bf70d..37f783c 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -3,6 +3,8 @@ pub(crate) mod ink; #[cfg(wl_tablet)] pub(crate) mod wl; +#[cfg(xinput2)] +pub(crate) mod xinput2; /// Holds any one of the internal platform IDs. /// Since these are always sealed away as an implementation detail, we can always @@ -12,6 +14,8 @@ pub(crate) mod wl; pub(crate) enum InternalID { #[cfg(wl_tablet)] Wayland(wl::ID), + #[cfg(xinput2)] + XInput2(xinput2::ID), #[cfg(ink_rts)] Ink(ink::ID), } @@ -54,6 +58,17 @@ impl InternalID { _ => Self::unwrap_failure(), } } + #[cfg(xinput2)] + #[inline] + #[allow(dead_code)] + pub(crate) fn unwrap_xinput2(&self) -> &xinput2::ID { + #[allow(unreachable_patterns)] + #[allow(clippy::match_wildcard_for_single_variants)] + match self { + Self::XInput2(id) => id, + _ => Self::unwrap_failure(), + } + } #[cfg(ink_rts)] #[inline] #[allow(dead_code)] @@ -72,6 +87,12 @@ impl From for InternalID { Self::Wayland(value) } } +#[cfg(xinput2)] +impl From for InternalID { + fn from(value: xinput2::ID) -> Self { + Self::XInput2(value) + } +} #[cfg(ink_rts)] impl From for InternalID { fn from(value: ink::ID) -> Self { @@ -86,6 +107,8 @@ impl From for InternalID { pub(crate) enum ButtonID { #[cfg(wl_tablet)] Wayland(wl::ButtonID), + #[cfg(xinput2)] + XInput2(xinput2::ButtonID), #[cfg(ink_rts)] Ink(ink::ButtonID), } @@ -128,6 +151,17 @@ impl ButtonID { _ => Self::unwrap_failure(), } } + #[cfg(xinput2)] + #[inline] + #[allow(dead_code)] + pub(crate) fn unwrap_xinput2(&self) -> &xinput2::ButtonID { + #[allow(unreachable_patterns)] + #[allow(clippy::match_wildcard_for_single_variants)] + match self { + Self::XInput2(id) => id, + _ => Self::unwrap_failure(), + } + } #[cfg(ink_rts)] #[inline] #[allow(dead_code)] @@ -146,6 +180,12 @@ impl From for ButtonID { Self::Wayland(value) } } +#[cfg(xinput2)] +impl From for ButtonID { + fn from(value: xinput2::ButtonID) -> Self { + Self::XInput2(value) + } +} #[cfg(ink_rts)] impl From for ButtonID { fn from(value: ink::ButtonID) -> Self { @@ -156,6 +196,8 @@ impl From for ButtonID { pub(crate) enum RawEventsIter<'a> { #[cfg(wl_tablet)] Wayland(std::slice::Iter<'a, crate::events::raw::Event>), + #[cfg(wl_tablet)] + XInput2(std::slice::Iter<'a, crate::events::raw::Event>), #[cfg(ink_rts)] Ink(std::slice::Iter<'a, crate::events::raw::Event>), } @@ -166,6 +208,8 @@ impl Iterator for RawEventsIter<'_> { match self { #[cfg(wl_tablet)] Self::Wayland(wl) => wl.next().cloned().map(crate::events::raw::Event::id_into), + #[cfg(xinput2)] + Self::XInput2(xi) => xi.next().cloned().map(crate::events::raw::Event::id_into), #[cfg(ink_rts)] Self::Ink(ink) => ink.next().cloned().map(crate::events::raw::Event::id_into), } @@ -190,12 +234,14 @@ pub(crate) trait PlatformImpl { } /// Static dispatch between compiled backends. -/// Enum cause why not, (almost?) always has one variant and is thus compiles away to the inner type transparently. +/// Enum cause why not, in some cases this has one variant and is thus compiles away to the inner type transparently. /// Even empty enum is OK, since everything involving it becomes essentially `match ! {}` which is sound :D #[enum_dispatch::enum_dispatch(PlatformImpl)] pub(crate) enum PlatformManager { #[cfg(wl_tablet)] Wayland(wl::Manager), + #[cfg(xinput2)] + XInput2(xinput2::Manager), #[cfg(ink_rts)] Ink(ink::Manager), } diff --git a/src/platform/xinput2/mod.rs b/src/platform/xinput2/mod.rs new file mode 100644 index 0000000..4882898 --- /dev/null +++ b/src/platform/xinput2/mod.rs @@ -0,0 +1,31 @@ +pub type ID = (); +pub type ButtonID = (); + +pub struct Manager; + +impl Manager { + pub unsafe fn build_window(_opts: crate::Builder, _window: u64) -> Self { + Self + } +} + +impl super::PlatformImpl for Manager { + fn pads(&self) -> &[crate::pad::Pad] { + &[] + } + fn pump(&mut self) -> Result<(), crate::PumpError> { + Ok(()) + } + fn raw_events(&self) -> super::RawEventsIter<'_> { + super::RawEventsIter::XInput2([].iter()) + } + fn tablets(&self) -> &[crate::tablet::Tablet] { + &[] + } + fn timestamp_granularity(&self) -> Option { + None + } + fn tools(&self) -> &[crate::tool::Tool] { + &[] + } +} From 0652a2a271a7dbeb28c4a2345db0c44ce91337ae Mon Sep 17 00:00:00 2001 From: Aspen Pratt Date: Sun, 26 May 2024 15:35:02 -0700 Subject: [PATCH 03/15] Switch to `x11rb`, allow Xcb handles. --- Cargo.lock | 10 +++++----- Cargo.toml | 8 +++++--- examples/sdl2.rs | 21 +++++++++++++++++++++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ea0fc7f..b1ce34f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2035,7 +2035,7 @@ dependencies = [ "wayland-protocols", "windows 0.54.0", "winit", - "x11-dl", + "x11rb", ] [[package]] @@ -3723,9 +3723,9 @@ dependencies = [ [[package]] name = "x11rb" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" dependencies = [ "as-raw-xcb-connection", "gethostname", @@ -3738,9 +3738,9 @@ dependencies = [ [[package]] name = "x11rb-protocol" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" [[package]] name = "xcursor" diff --git a/Cargo.toml b/Cargo.toml index 1d57241..0f4d1fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,8 +48,10 @@ smallvec = "1.13.1" wayland-backend = { version = "0.3.3", features = ["client_system"], optional = true } wayland-client = { version = "0.31.2", optional = true } wayland-protocols = { version = "0.31.2", features = ["client", "unstable"], optional = true } -# Xorg deps. -x11-dl = { version = "2.21.0", optional = true } + +# Xorg `xinput2` deps. We use x11rb RustConnection as xcb doesn't support xinput2 out-of-box +# and xlib cannot be soundly used due to its use of global variables. +x11rb = { version = "0.13.1", features = ["xinput", "extra-traits"], optional = true } # Windows Ink `RealTimeStylus` [target.'cfg(any(docsrs, target_os = "windows"))'.dependencies.windows] @@ -70,7 +72,7 @@ default = ["wayland-tablet-unstable-v2", "xorg-xinput2", "windows-ink"] # Note: "unstable" here refers to the protocol itself, not to the stability of it's integration into this crate! wayland-tablet-unstable-v2 = ["dep:wayland-backend", "dep:wayland-client", "dep:wayland-protocols"] -xorg-xinput2 = ["dep:x11-dl"] +xorg-xinput2 = ["dep:x11rb"] # Windows Ink `RealTimeStylus` support windows-ink = ["dep:windows"] diff --git a/examples/sdl2.rs b/examples/sdl2.rs index af09dbc..40bb2aa 100644 --- a/examples/sdl2.rs +++ b/examples/sdl2.rs @@ -34,6 +34,16 @@ mod rwh_bridge { screen, ) .into(), + // Xcb... + rwh_05::RawDisplayHandle::Xcb(rwh_05::XcbDisplayHandle { + connection, + screen, + .. + }) => raw_window_handle::XcbDisplayHandle::new( + std::ptr::NonNull::new(connection), + screen, + ) + .into(), // Windows 32... Has no display handle! rwh_05::RawDisplayHandle::Windows(_) => { raw_window_handle::WindowsDisplayHandle::new().into() @@ -70,6 +80,17 @@ mod rwh_bridge { rwh } .into(), + // Xcb... + rwh_05::RawWindowHandle::Xcb(rwh_05::XcbWindowHandle { + window, visual_id, .. + }) => { + let mut rwh = raw_window_handle::XcbWindowHandle::new( + std::num::NonZeroU32::new(window).expect("null xcb window"), + ); + rwh.visual_id = std::num::NonZeroU32::new(visual_id); + rwh + } + .into(), // Windows 32... rwh_05::RawWindowHandle::Win32(rwh_05::Win32WindowHandle { hinstance, From b2d6ed99eb673947e949a83d58e6d50dd20ad238 Mon Sep 17 00:00:00 2001 From: Aspen Pratt Date: Tue, 10 Sep 2024 21:10:07 -0700 Subject: [PATCH 04/15] Tools working, excuse the strongly-worded comments --- examples/winit-paint.rs | 8 +- src/builder.rs | 19 +- src/platform/mod.rs | 2 +- src/platform/xinput2/mod.rs | 922 +++++++++++++++++++++++++++++++++++- 4 files changed, 936 insertions(+), 15 deletions(-) diff --git a/examples/winit-paint.rs b/examples/winit-paint.rs index 70c2955..a31730a 100644 --- a/examples/winit-paint.rs +++ b/examples/winit-paint.rs @@ -199,6 +199,7 @@ fn main() { let event_loop = winit::event_loop::EventLoopBuilder::<()>::default() .build() .expect("start event loop"); + event_loop.listen_device_events(winit::event_loop::DeviceEvents::Always); let window = std::sync::Arc::new( winit::window::WindowBuilder::default() .with_inner_size(PhysicalSize::new(512u32, 512u32)) @@ -209,10 +210,15 @@ fn main() { // To allow us to draw on the screen without pulling in a whole GPU package, // we use `softbuffer` for presentation and `tiny-skia` for drawing - let mut pixmap = tiny_skia::Pixmap::new(512, 512).unwrap(); + let mut pixmap = + tiny_skia::Pixmap::new(window.inner_size().width, window.inner_size().height).unwrap(); let softbuffer = softbuffer::Context::new(window.as_ref()).expect("init softbuffer"); let mut surface = softbuffer::Surface::new(&softbuffer, &window).expect("make presentation surface"); + surface.resize( + window.inner_size().width.try_into().unwrap(), + window.inner_size().height.try_into().unwrap(), + ); // Fetch the tablets, using our window's handle for access. // Since we `Arc'd` our window, we get the safety of `build_shared`. Where this is not possible, diff --git a/src/builder.rs b/src/builder.rs index 1b8340c..c7f503e 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -121,19 +121,30 @@ impl Builder { #[cfg(xinput2)] raw_window_handle::RawDisplayHandle::Xlib(_) | raw_window_handle::RawDisplayHandle::Xcb(_) => { - // We don't actually care about the dispaly handle for xlib! We need the *window* id instead. + // We don't actually care about the dispaly handle for xlib or xcb! We need the *window* id instead. // (We manage our own connection and snoop the window events from there.) // As such, we accept both Xlib and Xcb, since we only care about the numeric window ID which is *server* defined, // not client library defined. let window = match rwh.window_handle()?.as_raw() { raw_window_handle::RawWindowHandle::Xlib( raw_window_handle::XlibWindowHandle { window, .. }, - ) => window, + ) => { + // u64 -> NonZeroU32 + u32::try_from(window) + .ok() + .and_then(|window| window.try_into().ok()) + } raw_window_handle::RawWindowHandle::Xcb( raw_window_handle::XcbWindowHandle { window, .. }, - ) => u64::from(window.get()), + ) => Some(window), // The display handle said it was one of these!! - _ => unreachable!(), + _ => None, + }; + + let Some(window) = window else { + return Err(BuildError::HandleError( + raw_window_handle::HandleError::Unavailable, + )); }; Ok(crate::platform::PlatformManager::XInput2( diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 37f783c..85b3b05 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -197,7 +197,7 @@ pub(crate) enum RawEventsIter<'a> { #[cfg(wl_tablet)] Wayland(std::slice::Iter<'a, crate::events::raw::Event>), #[cfg(wl_tablet)] - XInput2(std::slice::Iter<'a, crate::events::raw::Event>), + XInput2(std::slice::Iter<'a, crate::events::raw::Event>), #[cfg(ink_rts)] Ink(std::slice::Iter<'a, crate::events::raw::Event>), } diff --git a/src/platform/xinput2/mod.rs b/src/platform/xinput2/mod.rs index 4882898..8057a89 100644 --- a/src/platform/xinput2/mod.rs +++ b/src/platform/xinput2/mod.rs @@ -1,11 +1,772 @@ -pub type ID = (); -pub type ButtonID = (); +use crate::events::raw; +use x11rb::{ + connection::{Connection, RequestConnection}, + protocol::{ + xinput::{self, ConnectionExt}, + xproto::ConnectionExt as _, + }, +}; -pub struct Manager; +// Note: Device Product ID (286) property of tablets states USB vid and pid, and Device Node (285) +// lists path. Impl that pleas. thanks Uwu + +const XI_ALL_DEVICES: u16 = 0; +/// Magic timestamp signalling to the server "now". +const NOW_MAGIC: x11rb::protocol::xproto::Timestamp = 0; +// Strings are used to communicate the class of device, so we need a hueristic to +// find devices we are interested in and a transformation to a more well-documented enum. +// I Could not find a comprehensive guide to the strings used here. +// (I am SURE I saw one at one point, but can't find it again.) So these are just +// the ones I have access to in my testing. +/// X "device_type" atom for [`crate::tablet`]s... +const TYPE_TABLET: &str = "TABLET"; +/// .. [`crate::pad`]s... +const TYPE_PAD: &str = "PAD"; +/// .. [`crate::pad`]s also ?!?!?... +const TYPE_TOUCHPAD: &str = "TOUCHPAD"; +/// ..stylus tips.. +const TYPE_STYLUS: &str = "STYLUS"; +/// and erasers! +const TYPE_ERASER: &str = "ERASER"; +// Type "xwayland-pointer" is used for xwayland mice, styluses, erasers and... *squint* ...keyboards? +// The role could instead be parsed from it's user-facing device name: +// "xwayland-tablet-pad:" +// "xwayland-tablet eraser:" (note the hyphen becomes a space) +// "xwayland-tablet stylus:" +// Which is unfortunately a collapsed stream of all devices (similar to X's concept of a Master device) +// and thus all per-device info (names, hardware IDs, capabilities) is lost in abstraction. +const TYPE_XWAYLAND_POINTER: &str = "xwayland-pointer"; + +const TYPE_MOUSE: &str = "MOUSE"; +const TYPE_TOUCHSCREEN: &str = "TOUCHSCREEN"; + +/// Comes from `xinput_open_device`. Some APIs use u16. Confusing! +pub type ID = u8; +/// Comes from datasize of "button count" field of `ButtonInfo` - button names in xinput are indices, +/// with the zeroth index referring to the tool "down" state. +pub type ButtonID = std::num::NonZero; + +#[derive(Debug, Clone, Copy)] +enum ValuatorAxis { + // Absolute position, in a normalized device space. + // AbsX, + // AbsY, + AbsPressure, + // Degrees, -,- left and away from user. + AbsTiltX, + AbsTiltY, + // This pad ring, degrees, and maybe also stylus scrollwheel? I have none to test, + // but under Xwayland this capability is listed for both pad and stylus. + AbsWheel, +} +impl std::str::FromStr for ValuatorAxis { + type Err = (); + fn from_str(axis_label: &str) -> Result { + Ok(match axis_label { + // "Abs X" => Self::AbsX, + // "Abs Y" => Self::AbsY, + "Abs Pressure" => Self::AbsPressure, + "Abs Tilt X" => Self::AbsTiltX, + "Abs Tilt Y" => Self::AbsTiltY, + "Abs Wheel" => Self::AbsWheel, + // My guess is the next one is roll axis, but I do + // not have a any devices that report this axis. + _ => return Err(()), + }) + } +} +impl From for crate::axis::Axis { + fn from(value: ValuatorAxis) -> Self { + match value { + ValuatorAxis::AbsPressure => Self::Pressure, + ValuatorAxis::AbsTiltX | ValuatorAxis::AbsTiltY => Self::Tilt, + ValuatorAxis::AbsWheel => Self::Wheel, + //Self::AbsX | Self::AbsY => return None, + } + } +} +enum DeviceType { + Tool(crate::tool::Type), + Tablet, + Pad, +} +enum DeviceTypeOrXwayland { + Type(DeviceType), + /// Device type of xwayland-pointer doesn't tell us much, we must + /// also inspect the user-facing device name. + Xwayland, +} +impl std::str::FromStr for DeviceTypeOrXwayland { + type Err = (); + fn from_str(device_type: &str) -> Result { + use crate::tool::Type; + Ok(match device_type { + TYPE_STYLUS => Self::Type(DeviceType::Tool(Type::Pen)), + TYPE_ERASER => Self::Type(DeviceType::Tool(Type::Eraser)), + TYPE_PAD => Self::Type(DeviceType::Pad), + TYPE_TABLET => Self::Type(DeviceType::Tablet), + // TYPE_MOUSE => Self::Tool(Type::Mouse), + TYPE_XWAYLAND_POINTER => Self::Xwayland, + _ => return Err(()), + }) + } +} + +/// Parse the device name of an xwayland device, where the type is stored. +/// Use if [`DeviceType`] parsing came back as `DeviceType::Xwayland`. +fn xwayland_type_from_name(device_name: &str) -> Option { + use crate::tool::Type; + let class = device_name.strip_prefix("xwayland-tablet")?; + // there is a numeric field at the end, unclear what it means. + // For me, it's *always* `:43`, /shrug! + let colon = class.rfind(':')?; + let class = &class[..colon]; + + Some(match class { + // Weird inconsistent prefix xP + "-pad" => DeviceType::Pad, + " stylus" => DeviceType::Tool(Type::Pen), + " eraser" => DeviceType::Tool(Type::Eraser), + _ => return None, + }) +} + +#[derive(Copy, Clone)] +enum ToolName<'a> { + NameOnly(&'a str), + NameAndId(&'a str, crate::tool::HardwareID), +} +impl<'a> ToolName<'a> { + fn name(self) -> &'a str { + match self { + Self::NameAndId(name, _) | Self::NameOnly(name) => name, + } + } + fn id(self) -> Option { + match self { + Self::NameAndId(_, id) => Some(id), + Self::NameOnly(_) => None, + } + } +} + +/// From the user-facing Device name, try to parse a tool's hardware id. +fn tool_id_from_name(name: &str) -> ToolName { + // X11 seems to place tool hardware IDs within the human-readable Name of the device, and this is + // the only place it is exposed. Predictably, as with all things X, this is not documented as far + // as I can tell. From experience, it consists of the name, a space, and a hex number (or zero) + // in parentheses - This is a hueristic and likely non-exhaustive, Bleh. + + let try_parse = || -> Option<(&str, crate::tool::HardwareID)> { + // Detect the range of characters within the last set of parens. + let open_paren = name.rfind('(')?; + let after_open_paren = open_paren + 1; + // Find the close paren after the last open paren (weird change-of-base-address thing) + let close_paren = after_open_paren + name.get(after_open_paren..)?.find(')')?; + + // Find the human-readable name content, minus the id field. + let name_text = name[..open_paren].trim_ascii_end(); + + // Find the id field. + // id_text is literal '0', or a hexadecimal number prefixed by literal '0x' + let id_text = &name[after_open_paren..close_paren]; + + let id_num = if id_text == "0" { + // Should this be considered "None"? The XP-PEN DECO-01 reports this value, despite (afaik) + // lacking a genuine hardware ID capability. + 0 + } else if let Some(id_text) = id_text.strip_prefix("0x") { + u64::from_str_radix(id_text, 16).ok()? + } else { + return None; + }; + + Some((name_text, crate::tool::HardwareID(id_num))) + }; + + if let Some((name, id)) = try_parse() { + ToolName::NameAndId(name, id) + } else { + ToolName::NameOnly(name) + } +} +/// Turn an xinput fixed-point number into a float, rounded. +// I could probably keep them fixed for more maths, but this is easy for right now. +fn fixed32_to_f32(fixed: xinput::Fp3232) -> f32 { + // Could bit-twiddle these into place instead, likely with more precision. + let integral = fixed.integral as f32; + let fractional = fixed.frac as f32 / u32::MAX as f32; + + if fixed.integral.is_positive() { + integral + fractional + } else { + integral - fractional + } +} +/// Turn an xinput fixed-point number into a float, rounded. +// I could probably keep them fixed for more maths, but this is easy for right now. +fn fixed16_to_f32(fixed: i32) -> f32 { + (fixed as f32) / 65536.0 +} + +#[derive(Copy, Clone)] +enum Transform { + BiasScale { bias: f32, scale: f32 }, +} +impl Transform { + fn transform(&self, value: xinput::Fp3232) -> f32 { + let value = fixed32_to_f32(value); + match self { + Self::BiasScale { bias, scale } => (value + bias) * scale, + } + } +} + +#[derive(Copy, Clone)] +struct AxisInfo { + // Where in the valuator array is this? + index: u16, + // How to adapt the numeric value to octotablet's needs? + transform: Transform, +} + +/// Contains the metadata for translating a device's events to octotablet events. +struct ToolInfo { + pressure: Option, + tilt: [Option; 2], + wheel: Option, +} +impl ToolInfo { + fn axis_mut(&mut self, axis: ValuatorAxis) -> &mut Option { + match axis { + ValuatorAxis::AbsPressure => &mut self.pressure, + ValuatorAxis::AbsTiltX => &mut self.tilt[0], + ValuatorAxis::AbsTiltY => &mut self.tilt[1], + ValuatorAxis::AbsWheel => &mut self.wheel, + } + } +} + +struct PadInfo { + ring: Option, +} + +struct BitDiff { + bit_index: usize, + set: bool, +} + +struct BitDifferenceIter<'a> { + from: &'a [u32], + to: &'a [u32], + // cursor position: + // Which bit within the u32? + next_bit_idx: u32, + // Which word within the array? + cur_word: usize, +} +impl<'a> BitDifferenceIter<'a> { + fn diff(from: &'a [u32], to: &'a [u32]) -> Self { + Self { + from, + to, + next_bit_idx: 0, + cur_word: 0, + } + } +} +impl Iterator for BitDifferenceIter<'_> { + type Item = BitDiff; + fn next(&mut self) -> Option { + loop { + let to = self.to.get(self.cur_word)?; + let from = self.from.get(self.cur_word).copied().unwrap_or_default(); + let diff = to ^ from; + + // find the lowest set bit in the difference word. + let next_diff_idx = self.next_bit_idx + (diff >> self.next_bit_idx).trailing_zeros(); + + if next_diff_idx >= u32::BITS - 1 { + // Advance to the next word for next go around. + self.next_bit_idx = 0; + self.cur_word += 1; + } else { + // advance cursor regularly. + self.next_bit_idx = next_diff_idx + 1; + } + if next_diff_idx >= u32::BITS { + // No bit was set in this word, skip to next word. + continue; + } + + // Check what the difference we just found was. + let became_set = (to >> next_diff_idx) & 1 == 1; + + return Some(BitDiff { + bit_index: self.cur_word * u32::BITS as usize + next_diff_idx as usize, + set: became_set, + }); + } + } +} + +pub struct Manager { + conn: x11rb::rust_connection::RustConnection, + _xinput_minor_version: u16, + device_infos: std::collections::BTreeMap, + open_devices: Vec, + tools: Vec, + dummy_tablet: crate::tablet::Tablet, + events: Vec>, + window: x11rb::protocol::xproto::Window, +} impl Manager { - pub unsafe fn build_window(_opts: crate::Builder, _window: u64) -> Self { - Self + pub fn build_window(_opts: crate::Builder, window: std::num::NonZeroU32) -> Self { + let window = window.get(); + + let (conn, _screen) = x11rb::connect(None).unwrap(); + // Check we have XInput2 and get it's version. + conn.extension_information(xinput::X11_EXTENSION_NAME) + .unwrap() + .unwrap(); + let version = conn + // What the heck is "name"? it is totally undocumented and is not part of the XLib interface. + // I was unable to reverse engineer it, it seems to work regardless of what data is given to it. + .xinput_get_extension_version(b"Fixme!") + .unwrap() + .reply() + .unwrap(); + + assert!(version.present && version.server_major >= 2); + + // conn.xinput_select_extension_event( + // window, + // // Some crazy logic involving the output of OpenDevice. + // // /usr/include/X11/extensions/XInput.h has the macros that do the crime, however it seems nonportable to X11rb. + // // https://www.x.org/archive/X11R6.8.2/doc/XGetSelectedExtensionEvents.3.html + // &[u32::from(xinput::CHANGE_DEVICE_NOTIFY_EVENT)], + // ) + // .unwrap() + // .check() + // .unwrap(); + let hierarchy_interest = xinput::EventMask { + deviceid: XI_ALL_DEVICES, + mask: [ + // device add/remove/enable/disable. + xinput::XIEventMask::HIERARCHY, + ] + .into(), + }; + + // Ask for notification of device added/removed/reassigned. This is done before + // enumeration to avoid TOCTOU bug, but now the bug is in the opposite direction- + // We could enumerate a device *and* recieve an added message for it, or get a removal + // message for devices we never met. Beware! + conn.xinput_xi_select_events(window, std::slice::from_ref(&hierarchy_interest)) + .unwrap() + .check() + .unwrap(); + + // Testing with itty four button guy, + // TABLET = "Wacom Intuos S Pen" + // PAD = "Wacom Intuos S Pad" + // STYLUS = "Wacom Intuos S Pen Pen (0x7802cf3)" (no that isn't a typo lmao) + + // Fetch existing devices. It is important to do this after we requested to recieve `DEVICE_CHANGED` events, + // lest we run into TOCTOU bugs! + /* + let mut interest = vec![]; + */ + + let devices = conn.xinput_list_input_devices().unwrap().reply().unwrap(); + let mut flat_infos = &devices.infos[..]; + for (name, device) in devices.names.iter().zip(devices.devices.iter()) { + let ty = if device.device_type != 0 { + let mut ty = conn + .get_atom_name(device.device_type) + .unwrap() + .reply() + .unwrap() + .name; + ty.push(0); + std::ffi::CString::from_vec_with_nul(ty).ok() + } else { + None + }; + if let Ok(name) = std::str::from_utf8(&name.name) { + println!("{name}"); + } else { + println!(""); + } + /*if ty.as_deref() == Some(TYPE_STYLUS) || ty.as_deref() == Some(TYPE_ERASER) { + println!("^^ Binding ^^"); + //let _open = conn + // .xinput_open_device(device.device_id) + // .unwrap() + // .reply() + // .unwrap(); + //interest.push(device.device_id); + }*/ + println!(" {ty:?} - {device:?}"); + // Take the infos for this device from the list. + let infos = { + let (head, tail) = flat_infos.split_at(usize::from(device.num_class_info)); + flat_infos = tail; + head + }; + + for info in infos { + println!(" * {info:?}"); + } + } + /* + let mut interest = interest + .into_iter() + .map(|id| { + xinput::EventMask { + deviceid: id.into(), + mask: [ + // Cursor entering and leaving client area + xinput::XIEventMask::ENTER + | xinput::XIEventMask::LEAVE + // Barrel and tip buttons + | xinput::XIEventMask::BUTTON_PRESS + | xinput::XIEventMask::BUTTON_RELEASE + // Axis movement + | xinput::XIEventMask::MOTION, + ] + .into(), + } + }) + .collect::>(); + interest.push(xinput::EventMask { + deviceid: 0, + mask: [ + // Barrel and tip buttons + // device add/remove/capabilities changed. + xinput::XIEventMask::HIERARCHY, + ] + .into(), + }); + + // Register with the server that we want to listen in on these events for all current devices: + conn.xinput_xi_select_events(window, &interest) + .unwrap() + .check() + .unwrap();*/ + + // Future note for how to access core events, if needed. + // "XSelectInput" is just a wrapper over this, funny! + // https://github.com/mirror/libX11/blob/ff8706a5eae25b8bafce300527079f68a201d27f/src/SelInput.c#L33 + conn.change_window_attributes( + window, + &x11rb::protocol::xproto::ChangeWindowAttributesAux { + event_mask: Some(x11rb::protocol::xproto::EventMask::NO_EVENT), + ..Default::default() + }, + ) + .unwrap() + .check() + .unwrap(); + + let mut this = Self { + conn, + _xinput_minor_version: version.server_minor, + device_infos: std::collections::BTreeMap::new(), + open_devices: vec![], + tools: vec![], + dummy_tablet: crate::tablet::Tablet { + internal_id: super::InternalID::XInput2(0), + name: None, + usb_id: None, + }, + events: vec![], + window, + }; + + // Poll for devices. + this.repopulate(); + this + } + /// Close bound devices and enumerate server devices. Generates user-facing info structs and emits + /// change events accordingly. + #[allow(clippy::too_many_lines)] + fn repopulate(&mut self) { + // Fixme, hehe + self.tools.clear(); + + for device in self.open_devices.drain(..) { + self.conn + .xinput_close_device(device) + .unwrap() + .check() + .unwrap(); + } + // Tools ids to bulk-enable events on. + let mut tool_listen_events = vec![]; + + // Okay, this is weird. There are two very similar functions, xi_query_device and list_input_devices. + // The venne diagram of the data contained within their responses is nearly a circle, however each + // has subtle differences such that we need to query both and join the data. >~<; + let device_queries = self + .conn + .xinput_xi_query_device(XI_ALL_DEVICES) + .unwrap() + .reply() + .unwrap(); + + let device_list = self + .conn + .xinput_list_input_devices() + .unwrap() + .reply() + .unwrap(); + + // We recieve axis infos in a flat list, into which the individual devices refer. + // (mutable slice as we'll trim it as we consume) + let mut flat_infos = &device_list.infos[..]; + // We also recieve name strings in a parallel list. + for (name, device) in device_list + .names + .into_iter() + .zip(device_list.devices.into_iter()) + { + let _infos = { + // Split off however many axes this device claims. + let (infos, tail_infos) = flat_infos.split_at(device.num_class_info.into()); + flat_infos = tail_infos; + infos + }; + // Find the query that represents this device. + // Query and list contain very similar info, but both have tiny extra nuggets that we + // need. + let Some(query) = device_queries + .infos + .iter() + .find(|qdevice| qdevice.deviceid == u16::from(device.device_id)) + else { + continue; + }; + + // Query the "type" atom, which will describe what this device actually is through some heuristics. + // We can't use the capabilities it advertises as our detection method, since a lot of them are + // nonsensical (pad reporting absolute x,y, pressure, etc - but it doesn't do anything!) + if device.device_type == 0 { + // None. + continue; + } + let Some(device_type) = self + .conn + // This is *not* cached. Should we? We expect a small set of valid values, + // but on the other hand this isn't exactly a hot path. + .get_atom_name(device.device_type) + .ok() + // Whew! + .and_then(|response| response.reply().ok()) + .and_then(|atom| String::from_utf8(atom.name).ok()) + .and_then(|type_stirng| type_stirng.parse::().ok()) + else { + continue; + }; + + // UTF8 human-readable device name, which encodes some additional info sometimes. + let raw_name = String::from_utf8(name.name).ok(); + + let device_type = match device_type { + DeviceTypeOrXwayland::Type(t) => t, + // Generic xwayland type, parse the device name to find type instead. + DeviceTypeOrXwayland::Xwayland => { + let Some(ty) = raw_name.as_deref().and_then(xwayland_type_from_name) else { + // Couldn't figure out what the device is.. + continue; + }; + ty + } + }; + + // At this point, we're pretty sure this is a tool, pad, or tablet! + + if let DeviceType::Tool(tool_type) = device_type { + // It's a tool! Parse all relevant infos. + + // Try to parse the hardware ID from the name field. + let name_fields = raw_name.as_deref().map(tool_id_from_name); + + let mut octotablet_info = crate::tool::Tool { + internal_id: super::InternalID::XInput2(device.device_id), + name: name_fields.map(ToolName::name).map(ToOwned::to_owned), + hardware_id: name_fields.and_then(ToolName::id), + wacom_id: None, + tool_type: Some(tool_type), + axes: crate::axis::FullInfo::default(), + }; + + let mut x11_info = ToolInfo { + pressure: None, + tilt: [None, None], + wheel: None, + }; + + // Look for axes! + for class in &query.classes { + if let Some(v) = class.data.as_valuator() { + if v.mode != xinput::ValuatorMode::ABSOLUTE { + continue; + }; + // Weird case, that does happen in practice. :V + if v.min == v.max { + continue; + } + let Some(label) = self + .conn + .get_atom_name(v.label) + .ok() + .and_then(|response| response.reply().ok()) + .and_then(|atom| String::from_utf8(atom.name).ok()) + .and_then(|label| label.parse::().ok()) + else { + continue; + }; + + let min = fixed32_to_f32(v.min); + let max = fixed32_to_f32(v.max); + + match label { + ValuatorAxis::AbsPressure => { + // Scale and bias to [0,1]. + x11_info.pressure = Some(AxisInfo { + index: v.number, + transform: Transform::BiasScale { + bias: -min, + scale: 1.0 / (max - min), + }, + }); + octotablet_info.axes.pressure = + Some(crate::axis::NormalizedInfo { granularity: None }); + } + ValuatorAxis::AbsTiltX => { + // Seemingly always in degrees. + let deg_to_rad = 1.0f32.to_radians(); + x11_info.tilt[0] = Some(AxisInfo { + index: v.number, + transform: Transform::BiasScale { + bias: 0.0, + scale: deg_to_rad, + }, + }); + + let min = min.to_radians(); + let max = max.to_radians(); + + let new_info = crate::axis::Info { + limits: Some(crate::axis::Limits { + min: min.to_radians(), + max: max.to_radians(), + }), + granularity: None, + }; + + // Set the limits, or if already set take the union of the limits. + match &mut octotablet_info.axes.tilt { + slot @ None => *slot = Some(new_info), + Some(v) => match &mut v.limits { + slot @ None => *slot = new_info.limits, + Some(v) => { + v.max = v.max.max(max); + v.min = v.min.min(min); + } + }, + } + } + ValuatorAxis::AbsTiltY => { + // Seemingly always in degrees. + let deg_to_rad = 1.0f32.to_radians(); + x11_info.tilt[1] = Some(AxisInfo { + index: v.number, + transform: Transform::BiasScale { + bias: 0.0, + scale: deg_to_rad, + }, + }); + + let min = min.to_radians(); + let max = max.to_radians(); + + let new_info = crate::axis::Info { + limits: Some(crate::axis::Limits { + min: min.to_radians(), + max: max.to_radians(), + }), + granularity: None, + }; + + // Set the limits, or if already set take the union of the limits. + match &mut octotablet_info.axes.tilt { + slot @ None => *slot = Some(new_info), + Some(v) => match &mut v.limits { + slot @ None => *slot = new_info.limits, + Some(v) => { + v.max = v.max.max(max); + v.min = v.min.min(min); + } + }, + } + } + ValuatorAxis::AbsWheel => { + // uhh, i don't know. I have no hardware to test with. + } + } + + // Resolution is.. meaningless, I think. xwayland is the only server I have + // seen that even bothers to fill it out, and even there it's weird. + } + } + + // Request the server give us access to this device's events. + // Not sure what this reply data is for. + let _ = self + .conn + .xinput_open_device(device.device_id) + .unwrap() + .reply() + .unwrap(); + self.open_devices.push(device.device_id); + + tool_listen_events.push(device.device_id); + self.tools.push(octotablet_info); + self.device_infos.insert(device.device_id, x11_info); + } + } + + // Register with the server that we want to listen in on these events for all current devices: + let interest = tool_listen_events + .into_iter() + .map(|id| { + xinput::EventMask { + deviceid: id.into(), + mask: [ + // ..where is proximity? + + // Cursor entering and leaving client area (doesn't work lol) + xinput::XIEventMask::ENTER + | xinput::XIEventMask::LEAVE + // Barrel and tip buttons + | xinput::XIEventMask::BUTTON_PRESS + | xinput::XIEventMask::BUTTON_RELEASE + // Axis movement + | xinput::XIEventMask::MOTION, + ] + .into(), + } + }) + .collect::>(); + + self.conn + .xinput_xi_select_events(self.window, &interest) + .unwrap() + .check() + .unwrap(); } } @@ -13,19 +774,162 @@ impl super::PlatformImpl for Manager { fn pads(&self) -> &[crate::pad::Pad] { &[] } + #[allow(clippy::too_many_lines)] fn pump(&mut self) -> Result<(), crate::PumpError> { + self.events.clear(); + let mut has_repopulated = false; + + while let Ok(Some(event)) = self.conn.poll_for_event() { + use x11rb::protocol::Event; + match event { + Event::XinputProximityIn(x) => { + // never reported, :( + println!("In"); + } + Event::XinputProximityOut(x) => { + println!("Out"); + } + // XinputDeviceButtonPress, ButtonPress, XinputRawButtonPress are red herrings. + // Dear X consortium... What the fuck? + Event::XinputButtonPress(e) | Event::XinputButtonRelease(e) => { + if e.flags + .intersects(xinput::PointerEventFlags::POINTER_EMULATED) + { + // Key press emulation from scroll wheel. + continue; + } + let device_id = u8::try_from(e.deviceid).unwrap(); + if !self.device_infos.contains_key(&device_id) { + continue; + }; + + let button_idx = u16::try_from(e.detail).unwrap(); + + // Detail gives the "button index". + match button_idx { + // Doesn't occur, I don't think. + 0 => (), + // Tip button + 1 => { + if e.event_type == xinput::BUTTON_PRESS_EVENT { + self.events.push(raw::Event::Tool { + tool: device_id, + event: raw::ToolEvent::In { tablet: 0 }, + }); + self.events.push(raw::Event::Tool { + tool: device_id, + event: raw::ToolEvent::Down, + }); + } else { + self.events.push(raw::Event::Tool { + tool: device_id, + event: raw::ToolEvent::Up, + }); + self.events.push(raw::Event::Tool { + tool: device_id, + event: raw::ToolEvent::Out, + }); + } + } + // Other (barrel) button. + _ => { + self.events.push(raw::Event::Tool { + tool: device_id, + event: raw::ToolEvent::Button { + button_id: crate::platform::ButtonID::XInput2( + // Already checked != 0 + button_idx.try_into().unwrap(), + ), + pressed: e.event_type == xinput::BUTTON_PRESS_EVENT, + }, + }); + } + } + } + // Likewise, XinputMotion is a red herring. Grrr. + Event::XinputMotion(m) => { + let mut try_uwu = || -> Option<()> { + let device_id = m.deviceid.try_into().ok()?; + let info = self.device_infos.get(&device_id)?; + + let valuator_fetch = |idx: u16| -> Option { + // Check that it's not masked out- + let word_idx = idx / u32::BITS as u16; + let bit_idx = idx % u32::BITS as u16; + let word = m.valuator_mask.get(usize::from(word_idx))?; + + // This valuator did not report, value is undefined. + if word & (1 << bit_idx as u32) == 0 { + return None; + } + + // Fetch it! + m.axisvalues.get(usize::from(idx)).copied() + }; + // Access valuators, and map them to our range for the associated axis. + let pressure = info + .pressure + .and_then(|axis| { + Some(axis.transform.transform(valuator_fetch(axis.index)?)) + }) + .and_then(crate::util::NicheF32::new_some) + .unwrap_or(crate::util::NicheF32::NONE); + let tilt_x = info.tilt[0].and_then(|axis| { + Some(axis.transform.transform(valuator_fetch(axis.index)?)) + }); + let tilt_y = info.tilt[1].and_then(|axis| { + Some(axis.transform.transform(valuator_fetch(axis.index)?)) + }); + + self.events.push(raw::Event::Tool { + tool: device_id, + event: raw::ToolEvent::Pose(crate::axis::Pose { + // Seems to already be in logical space. + position: [fixed16_to_f32(m.event_x), fixed16_to_f32(m.event_y)], + distance: crate::util::NicheF32::NONE, + pressure, + button_pressure: crate::util::NicheF32::NONE, + tilt: match (tilt_x, tilt_y) { + (Some(x), Some(y)) => Some([x, y]), + (Some(x), None) => Some([x, 0.0]), + (None, Some(y)) => Some([0.0, y]), + (None, None) => None, + }, + roll: crate::util::NicheF32::NONE, + wheel: None, + slider: crate::util::NicheF32::NONE, + contact_size: None, + }), + }); + Some(()) + }; + if try_uwu().is_none() { + println!("failed to fetch axes."); + } + } + Event::XinputHierarchy(_) => { + // The event does not necessarily reflect *all* changes, the spec specifically says + // that the client should probably just rescan. lol + if !has_repopulated { + has_repopulated = true; + self.repopulate(); + } + } + other => println!("Other: {other:?}"), + } + } Ok(()) } fn raw_events(&self) -> super::RawEventsIter<'_> { - super::RawEventsIter::XInput2([].iter()) + super::RawEventsIter::XInput2(self.events.iter()) } fn tablets(&self) -> &[crate::tablet::Tablet] { - &[] + std::slice::from_ref(&self.dummy_tablet) } fn timestamp_granularity(&self) -> Option { - None + Some(std::time::Duration::from_millis(1)) } fn tools(&self) -> &[crate::tool::Tool] { - &[] + &self.tools } } From 963ce6a6e510e4f28b33d09bff07974c74f5fce5 Mon Sep 17 00:00:00 2001 From: Aspen Pratt Date: Wed, 11 Sep 2024 14:55:49 -0700 Subject: [PATCH 05/15] fix event enable logic, mediocre proximity events. --- src/events/mod.rs | 2 +- src/platform/xinput2/mod.rs | 516 +++++++++++++++++++++++++----------- 2 files changed, 356 insertions(+), 162 deletions(-) diff --git a/src/events/mod.rs b/src/events/mod.rs index dae91b0..d749c1f 100644 --- a/src/events/mod.rs +++ b/src/events/mod.rs @@ -252,7 +252,7 @@ impl<'manager> EventIterator<'manager> { .tablets() .iter() .find(|t| t.internal_id == tablet) - .unwrap(), + .ok_or(())?, }, RawTool::Down => ToolEvent::Down, RawTool::Button { button_id, pressed } => ToolEvent::Button { diff --git a/src/platform/xinput2/mod.rs b/src/platform/xinput2/mod.rs index 8057a89..c650cd8 100644 --- a/src/platform/xinput2/mod.rs +++ b/src/platform/xinput2/mod.rs @@ -316,9 +316,11 @@ pub struct Manager { device_infos: std::collections::BTreeMap, open_devices: Vec, tools: Vec, - dummy_tablet: crate::tablet::Tablet, + tablets: Vec, events: Vec>, window: x11rb::protocol::xproto::Window, + atom_usb_id: Option>, + atom_device_node: Option>, } impl Manager { @@ -395,7 +397,7 @@ impl Manager { None }; if let Ok(name) = std::str::from_utf8(&name.name) { - println!("{name}"); + println!("{} - {name}", device.device_id); } else { println!(""); } @@ -470,19 +472,28 @@ impl Manager { .check() .unwrap(); + let atom_usb_id = conn + .intern_atom(false, b"Device Product ID") + .ok() + .and_then(|resp| resp.reply().ok()) + .and_then(|reply| reply.atom.try_into().ok()); + let atom_device_node = conn + .intern_atom(false, b"Device Node") + .ok() + .and_then(|resp| resp.reply().ok()) + .and_then(|reply| reply.atom.try_into().ok()); + let mut this = Self { conn, _xinput_minor_version: version.server_minor, device_infos: std::collections::BTreeMap::new(), open_devices: vec![], tools: vec![], - dummy_tablet: crate::tablet::Tablet { - internal_id: super::InternalID::XInput2(0), - name: None, - usb_id: None, - }, events: vec![], + tablets: vec![], window, + atom_device_node, + atom_usb_id, }; // Poll for devices. @@ -493,8 +504,10 @@ impl Manager { /// change events accordingly. #[allow(clippy::too_many_lines)] fn repopulate(&mut self) { - // Fixme, hehe + // Fixme, hehe. We need to a) keep these alive for the next pump, and b) appropriately + // report adds/removes. self.tools.clear(); + self.tablets.clear(); for device in self.open_devices.drain(..) { self.conn @@ -587,156 +600,330 @@ impl Manager { // At this point, we're pretty sure this is a tool, pad, or tablet! - if let DeviceType::Tool(tool_type) = device_type { - // It's a tool! Parse all relevant infos. - - // Try to parse the hardware ID from the name field. - let name_fields = raw_name.as_deref().map(tool_id_from_name); - - let mut octotablet_info = crate::tool::Tool { - internal_id: super::InternalID::XInput2(device.device_id), - name: name_fields.map(ToolName::name).map(ToOwned::to_owned), - hardware_id: name_fields.and_then(ToolName::id), - wacom_id: None, - tool_type: Some(tool_type), - axes: crate::axis::FullInfo::default(), - }; - - let mut x11_info = ToolInfo { - pressure: None, - tilt: [None, None], - wheel: None, - }; - - // Look for axes! - for class in &query.classes { - if let Some(v) = class.data.as_valuator() { - if v.mode != xinput::ValuatorMode::ABSOLUTE { - continue; - }; - // Weird case, that does happen in practice. :V - if v.min == v.max { - continue; - } - let Some(label) = self - .conn - .get_atom_name(v.label) - .ok() - .and_then(|response| response.reply().ok()) - .and_then(|atom| String::from_utf8(atom.name).ok()) - .and_then(|label| label.parse::().ok()) - else { - continue; - }; + match device_type { + DeviceType::Tool(ty) => { + // It's a tool! Parse all relevant infos. - let min = fixed32_to_f32(v.min); - let max = fixed32_to_f32(v.max); - - match label { - ValuatorAxis::AbsPressure => { - // Scale and bias to [0,1]. - x11_info.pressure = Some(AxisInfo { - index: v.number, - transform: Transform::BiasScale { - bias: -min, - scale: 1.0 / (max - min), - }, - }); - octotablet_info.axes.pressure = - Some(crate::axis::NormalizedInfo { granularity: None }); - } - ValuatorAxis::AbsTiltX => { - // Seemingly always in degrees. - let deg_to_rad = 1.0f32.to_radians(); - x11_info.tilt[0] = Some(AxisInfo { - index: v.number, - transform: Transform::BiasScale { - bias: 0.0, - scale: deg_to_rad, - }, - }); + // Try to parse the hardware ID from the name field. + let name_fields = raw_name.as_deref().map(tool_id_from_name); - let min = min.to_radians(); - let max = max.to_radians(); - - let new_info = crate::axis::Info { - limits: Some(crate::axis::Limits { - min: min.to_radians(), - max: max.to_radians(), - }), - granularity: None, - }; - - // Set the limits, or if already set take the union of the limits. - match &mut octotablet_info.axes.tilt { - slot @ None => *slot = Some(new_info), - Some(v) => match &mut v.limits { - slot @ None => *slot = new_info.limits, - Some(v) => { - v.max = v.max.max(max); - v.min = v.min.min(min); - } - }, - } - } - ValuatorAxis::AbsTiltY => { - // Seemingly always in degrees. - let deg_to_rad = 1.0f32.to_radians(); - x11_info.tilt[1] = Some(AxisInfo { - index: v.number, - transform: Transform::BiasScale { - bias: 0.0, - scale: deg_to_rad, - }, - }); + let mut octotablet_info = crate::tool::Tool { + internal_id: super::InternalID::XInput2(device.device_id), + name: name_fields.map(ToolName::name).map(ToOwned::to_owned), + hardware_id: name_fields.and_then(ToolName::id), + wacom_id: None, + tool_type: Some(ty), + axes: crate::axis::FullInfo::default(), + }; - let min = min.to_radians(); - let max = max.to_radians(); - - let new_info = crate::axis::Info { - limits: Some(crate::axis::Limits { - min: min.to_radians(), - max: max.to_radians(), - }), - granularity: None, - }; - - // Set the limits, or if already set take the union of the limits. - match &mut octotablet_info.axes.tilt { - slot @ None => *slot = Some(new_info), - Some(v) => match &mut v.limits { - slot @ None => *slot = new_info.limits, - Some(v) => { - v.max = v.max.max(max); - v.min = v.min.min(min); - } - }, - } + let mut x11_info = ToolInfo { + pressure: None, + tilt: [None, None], + wheel: None, + }; + + // Look for axes! + for class in &query.classes { + if let Some(v) = class.data.as_valuator() { + if v.mode != xinput::ValuatorMode::ABSOLUTE { + continue; + }; + // Weird case, that does happen in practice. :V + if v.min == v.max { + continue; } - ValuatorAxis::AbsWheel => { - // uhh, i don't know. I have no hardware to test with. + let Some(label) = self + .conn + .get_atom_name(v.label) + .ok() + .and_then(|response| response.reply().ok()) + .and_then(|atom| String::from_utf8(atom.name).ok()) + .and_then(|label| label.parse::().ok()) + else { + continue; + }; + + let min = fixed32_to_f32(v.min); + let max = fixed32_to_f32(v.max); + + match label { + ValuatorAxis::AbsPressure => { + // Scale and bias to [0,1]. + x11_info.pressure = Some(AxisInfo { + index: v.number, + transform: Transform::BiasScale { + bias: -min, + scale: 1.0 / (max - min), + }, + }); + octotablet_info.axes.pressure = + Some(crate::axis::NormalizedInfo { granularity: None }); + } + ValuatorAxis::AbsTiltX => { + // Seemingly always in degrees. + let deg_to_rad = 1.0f32.to_radians(); + x11_info.tilt[0] = Some(AxisInfo { + index: v.number, + transform: Transform::BiasScale { + bias: 0.0, + scale: deg_to_rad, + }, + }); + + let min = min.to_radians(); + let max = max.to_radians(); + + let new_info = crate::axis::Info { + limits: Some(crate::axis::Limits { + min: min.to_radians(), + max: max.to_radians(), + }), + granularity: None, + }; + + // Set the limits, or if already set take the union of the limits. + match &mut octotablet_info.axes.tilt { + slot @ None => *slot = Some(new_info), + Some(v) => match &mut v.limits { + slot @ None => *slot = new_info.limits, + Some(v) => { + v.max = v.max.max(max); + v.min = v.min.min(min); + } + }, + } + } + ValuatorAxis::AbsTiltY => { + // Seemingly always in degrees. + let deg_to_rad = 1.0f32.to_radians(); + x11_info.tilt[1] = Some(AxisInfo { + index: v.number, + transform: Transform::BiasScale { + bias: 0.0, + scale: deg_to_rad, + }, + }); + + let min = min.to_radians(); + let max = max.to_radians(); + + let new_info = crate::axis::Info { + limits: Some(crate::axis::Limits { + min: min.to_radians(), + max: max.to_radians(), + }), + granularity: None, + }; + + // Set the limits, or if already set take the union of the limits. + match &mut octotablet_info.axes.tilt { + slot @ None => *slot = Some(new_info), + Some(v) => match &mut v.limits { + slot @ None => *slot = new_info.limits, + Some(v) => { + v.max = v.max.max(max); + v.min = v.min.min(min); + } + }, + } + } + ValuatorAxis::AbsWheel => { + // uhh, i don't know. I have no hardware to test with. + } } - } - // Resolution is.. meaningless, I think. xwayland is the only server I have - // seen that even bothers to fill it out, and even there it's weird. + // Resolution is.. meaningless, I think. xwayland is the only server I have + // seen that even bothers to fill it out, and even there it's weird. + } } + + tool_listen_events.push(device.device_id); + self.tools.push(octotablet_info); + self.device_infos.insert(device.device_id, x11_info); + } + DeviceType::Pad => { + continue; } + DeviceType::Tablet => { + // Tablets are of... dubious usefulness in xinput? + // They do not follow the paradigms needed by octotablet. + // Alas, we can still fetch some useful information! + let usb_id = self + .conn + // USBID consists of two 16 bit integers, [vid, pid]. + .xinput_get_device_property( + self.atom_usb_id.map_or(0, std::num::NonZero::get), + 0, + 0, + 2, + device.device_id, + false, + ) + .ok() + .and_then(|resp| resp.reply().ok()) + .and_then(|property| { + #[allow(clippy::get_first)] + // Try to accept any type. + Some(match property.items { + xinput::GetDevicePropertyItems::Data16(d) => crate::tablet::UsbId { + vid: *d.get(0)?, + pid: *d.get(1)?, + }, + xinput::GetDevicePropertyItems::Data8(d) => crate::tablet::UsbId { + vid: (*d.get(0)?).into(), + pid: (*d.get(1)?).into(), + }, + xinput::GetDevicePropertyItems::Data32(d) => crate::tablet::UsbId { + vid: (*d.get(0)?).try_into().ok()?, + pid: (*d.get(1)?).try_into().ok()?, + }, + xinput::GetDevicePropertyItems::InvalidValue(_) => return None, + }) + }); - // Request the server give us access to this device's events. - // Not sure what this reply data is for. - let _ = self - .conn - .xinput_open_device(device.device_id) - .unwrap() - .reply() - .unwrap(); - self.open_devices.push(device.device_id); + // We can also fetch device path here. + + let tablet = crate::tablet::Tablet { + internal_id: super::InternalID::XInput2(device.device_id), + name: raw_name, + usb_id, + }; + + self.tablets.push(tablet); + } + } - tool_listen_events.push(device.device_id); - self.tools.push(octotablet_info); - self.device_infos.insert(device.device_id, x11_info); + // If we got to this point, we accepted the device. + // Request the server give us access to this device's events. + // Not sure what this reply data is for. + let repl = self + .conn + .xinput_open_device(device.device_id) + .unwrap() + .reply() + .unwrap(); + // Keep track so we can close it later! + self.open_devices.push(device.device_id); + + // Enable event aspects. Why is this a different process than select events? + // Scientists are working day and night to find the answer. + let mut enable = Vec::<(u8, u8)>::new(); + for class in repl.class_info { + const DEVICE_KEY_PRESS: u8 = 0; + const DEVICE_KEY_RELEASE: u8 = 1; + const DEVICE_BUTTON_PRESS: u8 = 0; + const DEVICE_BUTTON_RELEASE: u8 = 1; + const DEVICE_MOTION_NOTIFY: u8 = 0; + const DEVICE_FOCUS_IN: u8 = 0; + const DEVICE_FOCUS_OUT: u8 = 1; + const PROXIMITY_IN: u8 = 0; + const PROXIMITY_OUT: u8 = 1; + const DEVICE_STATE_NOTIFY: u8 = 0; + const DEVICE_MAPPING_NOTIFY: u8 = 1; + const CHANGE_DEVICE_NOTIFY: u8 = 2; + // Reverse engineered from Xinput.h, and xinput/test.c + // #define FindTypeAndClass(device,proximity_in_type,desired_event_mask,ProximityClass,offset) \ + // FindTypeAndClass(device, proximity_in_type, desired_event_mask, ProximityClass, _proximityIn) + // == EXPANDED: == + // { + // int _i; + // XInputClassInfo *_ip; + // proximity_in_type = 0; + // desired_event_mask = 0; + // _i = 0; + // _ip = ((XDevice *) device)->classes; + // for (;_i< ((XDevice *) device)->num_classes; _i++, _ip++) { + // if (_ip->input_class == ProximityClass) { + // proximity_in_type = _ip->event_type_base + 0; + // desired_event_mask = ((XDevice *) device)->device_id << 8 | proximity_in_type; + // } + // } + // } + + // (base, offset) + + match class.class_id { + // Constants taken from XInput.h + xinput::InputClass::PROXIMITY => { + enable.extend_from_slice(&[ + (class.event_type_base, PROXIMITY_IN), + (class.event_type_base, PROXIMITY_OUT), + ]); + } + xinput::InputClass::BUTTON => { + enable.extend_from_slice(&[ + (class.event_type_base, DEVICE_BUTTON_PRESS), + (class.event_type_base, DEVICE_BUTTON_RELEASE), + ]); + } + xinput::InputClass::FOCUS => { + enable.extend_from_slice(&[ + (class.event_type_base, DEVICE_FOCUS_IN), + (class.event_type_base, DEVICE_FOCUS_OUT), + ]); + } + xinput::InputClass::OTHER => { + enable.extend_from_slice(&[ + (class.event_type_base, DEVICE_STATE_NOTIFY), + (class.event_type_base, DEVICE_MAPPING_NOTIFY), + (class.event_type_base, CHANGE_DEVICE_NOTIFY), + // PROPERTY_NOTIFY + ]); + } + xinput::InputClass::VALUATOR => { + enable.push((class.event_type_base, DEVICE_MOTION_NOTIFY)); + } + _ => (), + } } + let masks = enable + .into_iter() + .map(|(base, offset)| -> u32 { + u32::from(device.device_id) << 8 | (u32::from(base) + u32::from(offset)) + }) + .collect::>(); + + self.conn + .xinput_select_extension_event(self.window, &masks) + .unwrap() + .check() + .unwrap(); + let status = self + .conn + .xinput_grab_device( + self.window, + NOW_MAGIC, + x11rb::protocol::xproto::GrabMode::SYNC, + x11rb::protocol::xproto::GrabMode::SYNC, + false, + device.device_id, + &masks, + ) + .unwrap() + .reply() + .unwrap() + .status; + + println!("Grab {} - {:?}", device.device_id, status); + } + + if !self.tools.is_empty() { + // So.... xinput doesn't have the same "Tablet owns pads and tools" + // hierarchy as we do. When we associate tools with tablets, we need a tablet + // to bind it to, but xinput does not necessarily provide one. + + // Wacom tablets and the DECO-01 use a consistent naming scheme, where tools are called + // {Pen, Eraser} (hardware id), which we can use to extract such information. + self.tablets.push(crate::tablet::Tablet { + internal_id: super::InternalID::XInput2(0), + name: Some("xinput master".to_owned()), + usb_id: None, + }); + } + + // Skip if nothing to enable. (Avoids server error) + if tool_listen_events.is_empty() { + return; } // Register with the server that we want to listen in on these events for all current devices: @@ -746,16 +933,25 @@ impl Manager { xinput::EventMask { deviceid: id.into(), mask: [ - // ..where is proximity? - // Cursor entering and leaving client area (doesn't work lol) xinput::XIEventMask::ENTER | xinput::XIEventMask::LEAVE // Barrel and tip buttons | xinput::XIEventMask::BUTTON_PRESS | xinput::XIEventMask::BUTTON_RELEASE + // Also enter and leave? + | xinput::XIEventMask::FOCUS_IN + | xinput::XIEventMask::FOCUS_OUT + // No idea, doesn't send. + | xinput::XIEventMask::BARRIER_HIT + | xinput::XIEventMask::BARRIER_LEAVE + // Sent when a master device is bound, and the device controlling it + // changes (thus presenting a master with different classes) + // | xinput::XIEventMask::DEVICE_CHANGED + | xinput::XIEventMask::PROPERTY // Axis movement | xinput::XIEventMask::MOTION, + // Proximity is implicit, i guess. I'm losing my mind. ] .into(), } @@ -783,11 +979,17 @@ impl super::PlatformImpl for Manager { use x11rb::protocol::Event; match event { Event::XinputProximityIn(x) => { - // never reported, :( - println!("In"); + // x,device_id is total garbage? what did I do to deserve this fate. + self.events.push(raw::Event::Tool { + tool: *self.device_infos.keys().next().unwrap(), + event: raw::ToolEvent::In { tablet: 0 }, + }); } Event::XinputProximityOut(x) => { - println!("Out"); + self.events.push(raw::Event::Tool { + tool: *self.device_infos.keys().next().unwrap(), + event: raw::ToolEvent::Out, + }); } // XinputDeviceButtonPress, ButtonPress, XinputRawButtonPress are red herrings. // Dear X consortium... What the fuck? @@ -812,10 +1014,6 @@ impl super::PlatformImpl for Manager { // Tip button 1 => { if e.event_type == xinput::BUTTON_PRESS_EVENT { - self.events.push(raw::Event::Tool { - tool: device_id, - event: raw::ToolEvent::In { tablet: 0 }, - }); self.events.push(raw::Event::Tool { tool: device_id, event: raw::ToolEvent::Down, @@ -825,10 +1023,6 @@ impl super::PlatformImpl for Manager { tool: device_id, event: raw::ToolEvent::Up, }); - self.events.push(raw::Event::Tool { - tool: device_id, - event: raw::ToolEvent::Out, - }); } } // Other (barrel) button. @@ -924,7 +1118,7 @@ impl super::PlatformImpl for Manager { super::RawEventsIter::XInput2(self.events.iter()) } fn tablets(&self) -> &[crate::tablet::Tablet] { - std::slice::from_ref(&self.dummy_tablet) + &self.tablets } fn timestamp_granularity(&self) -> Option { Some(std::time::Duration::from_millis(1)) From 6a5314cb1adc1c75e101e3624730d5935d71440d Mon Sep 17 00:00:00 2001 From: Aspen Pratt Date: Wed, 11 Sep 2024 17:22:59 -0700 Subject: [PATCH 06/15] pad buttons, pad rings. --- src/platform/xinput2/mod.rs | 224 ++++++++++++++++++++++++++++++------ 1 file changed, 191 insertions(+), 33 deletions(-) diff --git a/src/platform/xinput2/mod.rs b/src/platform/xinput2/mod.rs index c650cd8..6fda4ba 100644 --- a/src/platform/xinput2/mod.rs +++ b/src/platform/xinput2/mod.rs @@ -206,6 +206,7 @@ fn fixed32_to_f32(fixed: xinput::Fp3232) -> f32 { /// Turn an xinput fixed-point number into a float, rounded. // I could probably keep them fixed for more maths, but this is easy for right now. fn fixed16_to_f32(fixed: i32) -> f32 { + // Could bit-twiddle these into place instead, likely with more precision. (fixed as f32) / 65536.0 } @@ -214,12 +215,14 @@ enum Transform { BiasScale { bias: f32, scale: f32 }, } impl Transform { - fn transform(&self, value: xinput::Fp3232) -> f32 { - let value = fixed32_to_f32(value); + fn transform(self, value: f32) -> f32 { match self { Self::BiasScale { bias, scale } => (value + bias) * scale, } } + fn transform_fixed(self, value: xinput::Fp3232) -> f32 { + self.transform(fixed32_to_f32(value)) + } } #[derive(Copy, Clone)] @@ -235,16 +238,11 @@ struct ToolInfo { pressure: Option, tilt: [Option; 2], wheel: Option, -} -impl ToolInfo { - fn axis_mut(&mut self, axis: ValuatorAxis) -> &mut Option { - match axis { - ValuatorAxis::AbsPressure => &mut self.pressure, - ValuatorAxis::AbsTiltX => &mut self.tilt[0], - ValuatorAxis::AbsTiltY => &mut self.tilt[1], - ValuatorAxis::AbsWheel => &mut self.wheel, - } - } + /// The tablet this tool belongs to, based on heuristics. + /// When "In" is fired, this is the device to reference, because X doesn't provide + /// such info. If none, uses a dummy tablet. + /// (tool -> tablet relationship is one-to-one-or-less in xinput instead of one-to-one-or-more as we expect) + tablet: Option, } struct PadInfo { @@ -313,9 +311,11 @@ impl Iterator for BitDifferenceIter<'_> { pub struct Manager { conn: x11rb::rust_connection::RustConnection, _xinput_minor_version: u16, - device_infos: std::collections::BTreeMap, + tool_infos: std::collections::BTreeMap, open_devices: Vec, tools: Vec, + pad_infos: std::collections::BTreeMap, + pads: Vec, tablets: Vec, events: Vec>, window: x11rb::protocol::xproto::Window, @@ -486,9 +486,11 @@ impl Manager { let mut this = Self { conn, _xinput_minor_version: version.server_minor, - device_infos: std::collections::BTreeMap::new(), + tool_infos: std::collections::BTreeMap::new(), + pad_infos: std::collections::BTreeMap::new(), open_devices: vec![], tools: vec![], + pads: vec![], events: vec![], tablets: vec![], window, @@ -508,6 +510,8 @@ impl Manager { // report adds/removes. self.tools.clear(); self.tablets.clear(); + self.tool_infos.clear(); + self.pad_infos.clear(); for device in self.open_devices.drain(..) { self.conn @@ -620,6 +624,7 @@ impl Manager { pressure: None, tilt: [None, None], wheel: None, + tablet: None, }; // Look for axes! @@ -739,10 +744,79 @@ impl Manager { tool_listen_events.push(device.device_id); self.tools.push(octotablet_info); - self.device_infos.insert(device.device_id, x11_info); + self.tool_infos.insert(device.device_id, x11_info); } DeviceType::Pad => { - continue; + let mut buttons = 0; + let mut ring_info = None; + for class in &query.classes { + match &class.data { + xinput::DeviceClassData::Button(b) => { + buttons = b.num_buttons(); + } + xinput::DeviceClassData::Valuator(v) => { + // Look for and bind an "Abs Wheel" which is our ring. + if v.mode != xinput::ValuatorMode::ABSOLUTE { + continue; + } + let Some(label) = self + .conn + .get_atom_name(v.label) + .ok() + .and_then(|response| response.reply().ok()) + .and_then(|atom| String::from_utf8(atom.name).ok()) + .and_then(|label| label.parse::().ok()) + else { + continue; + }; + if matches!(label, ValuatorAxis::AbsWheel) { + // Remap to [0, TAU], clockwise from logical north. + if v.min == v.max { + continue; + } + let min = fixed32_to_f32(v.min); + let max = fixed32_to_f32(v.max); + ring_info = Some(AxisInfo { + index: v.number, + transform: Transform::BiasScale { + bias: -min, + scale: std::f32::consts::TAU / (max - min), + }, + }); + } + } + _ => (), + } + } + if buttons == 0 && ring_info.is_none() { + // This pad has no functionality for us. + continue; + } + + let mut rings = vec![]; + if ring_info.is_some() { + rings.push(crate::pad::Ring { + granularity: None, + internal_id: crate::platform::InternalID::XInput2(device.device_id), + }); + }; + // X11 has no concept of groups (i don't .. think?) + // So make a single group that owns everything. + let group = crate::pad::Group { + buttons: (0..buttons).map(Into::into).collect::>(), + feedback: None, + internal_id: crate::platform::InternalID::XInput2(device.device_id), + mode_count: None, + rings, + strips: vec![], + }; + self.pads.push(crate::pad::Pad { + internal_id: crate::platform::InternalID::XInput2(device.device_id), + total_buttons: buttons.into(), + groups: vec![group], + }); + self.pad_infos + .insert(device.device_id, PadInfo { ring: ring_info }); } DeviceType::Tablet => { // Tablets are of... dubious usefulness in xinput? @@ -873,6 +947,12 @@ impl Manager { xinput::InputClass::VALUATOR => { enable.push((class.event_type_base, DEVICE_MOTION_NOTIFY)); } + xinput::InputClass::KEY => { + enable.extend_from_slice(&[ + (class.event_type_base, DEVICE_KEY_PRESS), + (class.event_type_base, DEVICE_KEY_RELEASE), + ]); + } _ => (), } } @@ -888,7 +968,7 @@ impl Manager { .unwrap() .check() .unwrap(); - let status = self + /*let status = self .conn .xinput_grab_device( self.window, @@ -904,7 +984,7 @@ impl Manager { .unwrap() .status; - println!("Grab {} - {:?}", device.device_id, status); + println!("Grab {} - {:?}", device.device_id, status);*/ } if !self.tools.is_empty() { @@ -967,9 +1047,6 @@ impl Manager { } impl super::PlatformImpl for Manager { - fn pads(&self) -> &[crate::pad::Pad] { - &[] - } #[allow(clippy::too_many_lines)] fn pump(&mut self) -> Result<(), crate::PumpError> { self.events.clear(); @@ -979,21 +1056,22 @@ impl super::PlatformImpl for Manager { use x11rb::protocol::Event; match event { Event::XinputProximityIn(x) => { - // x,device_id is total garbage? what did I do to deserve this fate. + // x,device_id is total garbage? what did I do to deserve this fate.- self.events.push(raw::Event::Tool { - tool: *self.device_infos.keys().next().unwrap(), + tool: *self.tool_infos.keys().next().unwrap(), event: raw::ToolEvent::In { tablet: 0 }, }); } Event::XinputProximityOut(x) => { self.events.push(raw::Event::Tool { - tool: *self.device_infos.keys().next().unwrap(), + tool: *self.tool_infos.keys().next().unwrap(), event: raw::ToolEvent::Out, }); } // XinputDeviceButtonPress, ButtonPress, XinputRawButtonPress are red herrings. // Dear X consortium... What the fuck? Event::XinputButtonPress(e) | Event::XinputButtonRelease(e) => { + // Tool buttons. if e.flags .intersects(xinput::PointerEventFlags::POINTER_EMULATED) { @@ -1001,7 +1079,7 @@ impl super::PlatformImpl for Manager { continue; } let device_id = u8::try_from(e.deviceid).unwrap(); - if !self.device_infos.contains_key(&device_id) { + if !self.tool_infos.contains_key(&device_id) { continue; }; @@ -1040,11 +1118,10 @@ impl super::PlatformImpl for Manager { } } } - // Likewise, XinputMotion is a red herring. Grrr. Event::XinputMotion(m) => { + // Tool valuators. let mut try_uwu = || -> Option<()> { let device_id = m.deviceid.try_into().ok()?; - let info = self.device_infos.get(&device_id)?; let valuator_fetch = |idx: u16| -> Option { // Check that it's not masked out- @@ -1060,19 +1137,20 @@ impl super::PlatformImpl for Manager { // Fetch it! m.axisvalues.get(usize::from(idx)).copied() }; + let tool_info = self.tool_infos.get(&device_id)?; // Access valuators, and map them to our range for the associated axis. - let pressure = info + let pressure = tool_info .pressure .and_then(|axis| { - Some(axis.transform.transform(valuator_fetch(axis.index)?)) + Some(axis.transform.transform_fixed(valuator_fetch(axis.index)?)) }) .and_then(crate::util::NicheF32::new_some) .unwrap_or(crate::util::NicheF32::NONE); - let tilt_x = info.tilt[0].and_then(|axis| { - Some(axis.transform.transform(valuator_fetch(axis.index)?)) + let tilt_x = tool_info.tilt[0].and_then(|axis| { + Some(axis.transform.transform_fixed(valuator_fetch(axis.index)?)) }); - let tilt_y = info.tilt[1].and_then(|axis| { - Some(axis.transform.transform(valuator_fetch(axis.index)?)) + let tilt_y = tool_info.tilt[1].and_then(|axis| { + Some(axis.transform.transform_fixed(valuator_fetch(axis.index)?)) }); self.events.push(raw::Event::Tool { @@ -1101,6 +1179,82 @@ impl super::PlatformImpl for Manager { println!("failed to fetch axes."); } } + Event::XinputDeviceValuator(m) => { + // Pad valuators. Instead of the arbtrary number of valuators that the tools + // are sent, this sends in groups of six. Ignore all of them except the packet that + // contains our ring value. + + if let Some(pad_info) = self.pad_infos.get(&m.device_id) { + let Some(ring_info) = pad_info.ring else { + continue; + }; + let absolute_ring_index = ring_info.index; + let Some(relative_ring_indox) = + absolute_ring_index.checked_sub(u16::from(m.first_valuator)) + else { + continue; + }; + if relative_ring_indox >= m.num_valuators.into() { + continue; + } + + let Some(&valuator_value) = + m.valuators.get(usize::from(relative_ring_indox)) + else { + continue; + }; + + if valuator_value == 0 { + // On release, this is snapped back to zero, but zero is also a valid value. There does not + // seem to be a method of checking when the interaction ended to avoid this. + + // Snapping back to zero makes this entirely useless for knob control (which is the primary + // purpose of the ring) so we take this little loss. + continue; + } + + self.events.push(raw::Event::Pad { + pad: m.device_id, + event: raw::PadEvent::Group { + group: m.device_id, + event: raw::PadGroupEvent::Ring { + ring: m.device_id, + event: crate::events::TouchStripEvent::Pose( + ring_info.transform.transform(valuator_value as f32), + ), + }, + }, + }); + } + } + Event::XinputDeviceButtonPress(e) | Event::XinputDeviceButtonRelease(e) => { + // Pad buttons. + let Some(pad) = self + .pads + .iter() + .find(|pad| *pad.internal_id.unwrap_xinput2() == e.device_id) + else { + continue; + }; + + let button_idx = u32::from(e.detail); + if button_idx == 0 || pad.total_buttons < button_idx { + // Okay, there's a weird off-by-one here, that even throws off the `xinput` debug + // utility. My Intuos Pro S reports 11 buttons, but the maximum button index is.... 11, + // which is clearly invalid. Silly. + // I interpret this as it actually being [1, max_button] instead of [0, max_button) + continue; + } + + self.events.push(raw::Event::Pad { + pad: e.device_id, + event: raw::PadEvent::Button { + // Shift 1-based to 0-based indexing. + button_idx: button_idx - 1, + pressed: e.response_type == 69, + }, + }); + } Event::XinputHierarchy(_) => { // The event does not necessarily reflect *all* changes, the spec specifically says // that the client should probably just rescan. lol @@ -1110,6 +1264,7 @@ impl super::PlatformImpl for Manager { } } other => println!("Other: {other:?}"), + //_ => (), } } Ok(()) @@ -1120,6 +1275,9 @@ impl super::PlatformImpl for Manager { fn tablets(&self) -> &[crate::tablet::Tablet] { &self.tablets } + fn pads(&self) -> &[crate::pad::Pad] { + &self.pads + } fn timestamp_granularity(&self) -> Option { Some(std::time::Duration::from_millis(1)) } From efff1b582ffc704e4ccde726d6fb43805f9df1b4 Mon Sep 17 00:00:00 2001 From: Aspen Pratt Date: Thu, 12 Sep 2024 13:05:22 -0700 Subject: [PATCH 07/15] Fix eframe-viewer scale factor logic --- examples/eframe-viewer/main.rs | 16 ++++++++-------- examples/eframe-viewer/state.rs | 20 ++++++++++++++++++-- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/examples/eframe-viewer/main.rs b/examples/eframe-viewer/main.rs index d72baf8..830b013 100644 --- a/examples/eframe-viewer/main.rs +++ b/examples/eframe-viewer/main.rs @@ -53,14 +53,12 @@ impl EventFilter { }, Event::Tablet { .. } => true, Event::Pad { event, .. } => match event { - PadEvent::Group { event, .. } => match event { - PadGroupEvent::Ring { event, .. } | PadGroupEvent::Strip { event, .. } => { - match event { - TouchStripEvent::Frame(..) => self.frames, - TouchStripEvent::Pose(..) => self.poses, - _ => true, - } - } + PadEvent::Group { + event: PadGroupEvent::Ring { event, .. } | PadGroupEvent::Strip { event, .. }, + .. + } => match event { + TouchStripEvent::Frame(..) => self.frames, + TouchStripEvent::Pose(..) => self.poses, _ => true, }, _ => true, @@ -215,6 +213,8 @@ impl eframe::App for Viewer { }); }); + // Set the scale factor. Notably, this does *not* include the window's scale factor! + self.state.egui_scale_factor = ctx.zoom_factor(); // update the state with the new events! self.state.extend(events); diff --git a/examples/eframe-viewer/state.rs b/examples/eframe-viewer/state.rs index ca71163..901109b 100644 --- a/examples/eframe-viewer/state.rs +++ b/examples/eframe-viewer/state.rs @@ -38,7 +38,6 @@ fn radial_delta(from: f32, to: f32) -> f32 { nearest_delta.unwrap() } -#[derive(Default)] pub struct State { /// State of any `In` tools, removed when they go `Out`. /// Note that this isn't a singleton! Several tools can be active at the same time @@ -49,6 +48,20 @@ pub struct State { strips: collections::HashMap, /// The position of a virtual knob to show off slider/ring states. knob_pos: f32, + /// Egui's scale factor. octotablet gives us positions in logical window space, but we need + /// to draw in egui's coordinate space. + pub egui_scale_factor: f32, +} +impl Default for State { + fn default() -> Self { + Self { + tools: collections::HashMap::new(), + rings: collections::HashMap::new(), + strips: collections::HashMap::new(), + knob_pos: 0.0, + egui_scale_factor: 1.0, + } + } } impl<'a> Extend> for State { fn extend>>(&mut self, iter: T) { @@ -80,10 +93,13 @@ impl<'a> Extend> for State { }; tool.down = false; } - ToolEvent::Pose(pose) => { + ToolEvent::Pose(mut pose) => { let Some(tool) = self.tools.get_mut(&tool.id()) else { continue; }; + // Remap from logical window pixels to egui points. + pose.position[0] /= self.egui_scale_factor; + pose.position[1] /= self.egui_scale_factor; // Limited size circular buf - pop to make room if full. if tool.path.len() == PATH_LEN { tool.path.pop_front(); From 85f048367c3f518e32f4519804fc785f463d9bd5 Mon Sep 17 00:00:00 2001 From: Aspen Pratt Date: Thu, 12 Sep 2024 18:51:51 -0700 Subject: [PATCH 08/15] device grabs, better in/out emulation. --- examples/winit-paint.rs | 11 +- src/platform/xinput2/mod.rs | 217 +++++++++++++++++++++++++++++------- 2 files changed, 184 insertions(+), 44 deletions(-) diff --git a/examples/winit-paint.rs b/examples/winit-paint.rs index a31730a..e2b3dd0 100644 --- a/examples/winit-paint.rs +++ b/examples/winit-paint.rs @@ -159,6 +159,7 @@ impl Painter { } // Positioning data, continue drawing! ToolEvent::Pose(mut pose) => { + println!("{:?} - x {}", tool.name, pose.position[0]); // If there's a painter, paint on it! // If not, we haven't hit the `Down` event yet. if let Some(painter) = self.tools.get_mut(&tool.id()) { @@ -215,10 +216,12 @@ fn main() { let softbuffer = softbuffer::Context::new(window.as_ref()).expect("init softbuffer"); let mut surface = softbuffer::Surface::new(&softbuffer, &window).expect("make presentation surface"); - surface.resize( - window.inner_size().width.try_into().unwrap(), - window.inner_size().height.try_into().unwrap(), - ); + surface + .resize( + window.inner_size().width.try_into().unwrap(), + window.inner_size().height.try_into().unwrap(), + ) + .unwrap(); // Fetch the tablets, using our window's handle for access. // Since we `Arc'd` our window, we get the safety of `build_shared`. Where this is not possible, diff --git a/src/platform/xinput2/mod.rs b/src/platform/xinput2/mod.rs index 6fda4ba..9710d4a 100644 --- a/src/platform/xinput2/mod.rs +++ b/src/platform/xinput2/mod.rs @@ -3,7 +3,7 @@ use x11rb::{ connection::{Connection, RequestConnection}, protocol::{ xinput::{self, ConnectionExt}, - xproto::ConnectionExt as _, + xproto::{ConnectionExt as _, Timestamp}, }, }; @@ -11,6 +11,7 @@ use x11rb::{ // lists path. Impl that pleas. thanks Uwu const XI_ALL_DEVICES: u16 = 0; +const XI_ALL_MASTER_DEVICES: u8 = 1; /// Magic timestamp signalling to the server "now". const NOW_MAGIC: x11rb::protocol::xproto::Timestamp = 0; // Strings are used to communicate the class of device, so we need a hueristic to @@ -37,6 +38,8 @@ const TYPE_ERASER: &str = "ERASER"; // and thus all per-device info (names, hardware IDs, capabilities) is lost in abstraction. const TYPE_XWAYLAND_POINTER: &str = "xwayland-pointer"; +const EMULATED_TABLET_NAME: &str = "octotablet emulated"; + const TYPE_MOUSE: &str = "MOUSE"; const TYPE_TOUCHSCREEN: &str = "TOUCHSCREEN"; @@ -195,7 +198,7 @@ fn tool_id_from_name(name: &str) -> ToolName { fn fixed32_to_f32(fixed: xinput::Fp3232) -> f32 { // Could bit-twiddle these into place instead, likely with more precision. let integral = fixed.integral as f32; - let fractional = fixed.frac as f32 / u32::MAX as f32; + let fractional = fixed.frac as f32 / (u64::from(u32::MAX) + 1) as f32; if fixed.integral.is_positive() { integral + fractional @@ -233,6 +236,14 @@ struct AxisInfo { transform: Transform, } +#[derive(Eq, PartialEq)] +enum Phase { + In, + Down, + Up, + Out, +} + /// Contains the metadata for translating a device's events to octotablet events. struct ToolInfo { pressure: Option, @@ -243,6 +254,10 @@ struct ToolInfo { /// such info. If none, uses a dummy tablet. /// (tool -> tablet relationship is one-to-one-or-less in xinput instead of one-to-one-or-more as we expect) tablet: Option, + phase: Phase, + /// The master cursor. Grab this device when this cursor Enters, release it when it + /// leaves. + master: u16, } struct PadInfo { @@ -308,11 +323,16 @@ impl Iterator for BitDifferenceIter<'_> { } } +struct OpenDevice { + mask: Vec, + id: ID, +} + pub struct Manager { conn: x11rb::rust_connection::RustConnection, _xinput_minor_version: u16, tool_infos: std::collections::BTreeMap, - open_devices: Vec, + open_devices: Vec, tools: Vec, pad_infos: std::collections::BTreeMap, pads: Vec, @@ -515,17 +535,20 @@ impl Manager { for device in self.open_devices.drain(..) { self.conn - .xinput_close_device(device) + .xinput_close_device(device.id) .unwrap() .check() .unwrap(); } // Tools ids to bulk-enable events on. - let mut tool_listen_events = vec![]; + let mut tool_listen_events = vec![XI_ALL_MASTER_DEVICES]; // Okay, this is weird. There are two very similar functions, xi_query_device and list_input_devices. // The venne diagram of the data contained within their responses is nearly a circle, however each // has subtle differences such that we need to query both and join the data. >~<; + + // "Clients are requested to avoid mixing XI1.x and XI2 code as much as possible" well then maybe + // you shoulda made query_device actually return all the necessary data ya silly goober. let device_queries = self .conn .xinput_xi_query_device(XI_ALL_DEVICES) @@ -589,7 +612,6 @@ impl Manager { // UTF8 human-readable device name, which encodes some additional info sometimes. let raw_name = String::from_utf8(name.name).ok(); - let device_type = match device_type { DeviceTypeOrXwayland::Type(t) => t, // Generic xwayland type, parse the device name to find type instead. @@ -608,6 +630,16 @@ impl Manager { DeviceType::Tool(ty) => { // It's a tool! Parse all relevant infos. + // We can only handle tools which have a parent. + // (and obviously they shouldn't be a keyboard.) + // Technically, a floating pointer can work for our needs, + // but it behaves weird when not grabbed and it's not easy to know + // when to grab/release a floating device. + // (We could manually implement a hit test? yikes) + if query.type_ != xinput::DeviceType::SLAVE_POINTER { + continue; + } + // Try to parse the hardware ID from the name field. let name_fields = raw_name.as_deref().map(tool_id_from_name); @@ -625,6 +657,8 @@ impl Manager { tilt: [None, None], wheel: None, tablet: None, + phase: Phase::Out, + master: query.attachment, }; // Look for axes! @@ -825,13 +859,13 @@ impl Manager { let usb_id = self .conn // USBID consists of two 16 bit integers, [vid, pid]. - .xinput_get_device_property( + .xinput_xi_get_property( + device.device_id, + false, self.atom_usb_id.map_or(0, std::num::NonZero::get), 0, 0, 2, - device.device_id, - false, ) .ok() .and_then(|resp| resp.reply().ok()) @@ -839,19 +873,19 @@ impl Manager { #[allow(clippy::get_first)] // Try to accept any type. Some(match property.items { - xinput::GetDevicePropertyItems::Data16(d) => crate::tablet::UsbId { + xinput::XIGetPropertyItems::Data16(d) => crate::tablet::UsbId { vid: *d.get(0)?, pid: *d.get(1)?, }, - xinput::GetDevicePropertyItems::Data8(d) => crate::tablet::UsbId { + xinput::XIGetPropertyItems::Data8(d) => crate::tablet::UsbId { vid: (*d.get(0)?).into(), pid: (*d.get(1)?).into(), }, - xinput::GetDevicePropertyItems::Data32(d) => crate::tablet::UsbId { + xinput::XIGetPropertyItems::Data32(d) => crate::tablet::UsbId { vid: (*d.get(0)?).try_into().ok()?, pid: (*d.get(1)?).try_into().ok()?, }, - xinput::GetDevicePropertyItems::InvalidValue(_) => return None, + xinput::XIGetPropertyItems::InvalidValue(_) => return None, }) }); @@ -876,8 +910,6 @@ impl Manager { .unwrap() .reply() .unwrap(); - // Keep track so we can close it later! - self.open_devices.push(device.device_id); // Enable event aspects. Why is this a different process than select events? // Scientists are working day and night to find the answer. @@ -963,6 +995,12 @@ impl Manager { }) .collect::>(); + // Keep track so we can close it later! + self.open_devices.push(OpenDevice { + mask: masks.clone(), + id: device.device_id, + }); + self.conn .xinput_select_extension_event(self.window, &masks) .unwrap() @@ -996,7 +1034,7 @@ impl Manager { // {Pen, Eraser} (hardware id), which we can use to extract such information. self.tablets.push(crate::tablet::Tablet { internal_id: super::InternalID::XInput2(0), - name: Some("xinput master".to_owned()), + name: Some(EMULATED_TABLET_NAME.to_owned()), usb_id: None, }); } @@ -1013,25 +1051,28 @@ impl Manager { xinput::EventMask { deviceid: id.into(), mask: [ - // Cursor entering and leaving client area (doesn't work lol) - xinput::XIEventMask::ENTER - | xinput::XIEventMask::LEAVE // Barrel and tip buttons - | xinput::XIEventMask::BUTTON_PRESS + xinput::XIEventMask::BUTTON_PRESS | xinput::XIEventMask::BUTTON_RELEASE - // Also enter and leave? + // Cursor entering and leaving client area. Doesn't work. + | xinput::XIEventMask::ENTER + | xinput::XIEventMask::LEAVE + // Also enter and leave? Doesn't work. | xinput::XIEventMask::FOCUS_IN | xinput::XIEventMask::FOCUS_OUT - // No idea, doesn't send. + // No idea, undocumented and doesn't work. | xinput::XIEventMask::BARRIER_HIT | xinput::XIEventMask::BARRIER_LEAVE - // Sent when a master device is bound, and the device controlling it - // changes (thus presenting a master with different classes) - // | xinput::XIEventMask::DEVICE_CHANGED - | xinput::XIEventMask::PROPERTY // Axis movement | xinput::XIEventMask::MOTION, // Proximity is implicit, i guess. I'm losing my mind. + + // property change. The only properties we look at are static. + // | xinput::XIEventMask::PROPERTY + // Sent when a master device is bound, and the device controlling it + // changes (thus presenting a master with different classes) + // We don't care about master devices, though! + // | xinput::XIEventMask::DEVICE_CHANGED ] .into(), } @@ -1044,6 +1085,65 @@ impl Manager { .check() .unwrap(); } + fn parent_entered(&mut self, master: u16, time: Timestamp) { + for device in &self.open_devices { + let Some(tool) = self.tool_infos.get_mut(&device.id) else { + continue; + }; + if master != tool.master { + continue; + } + + // Don't care if it succeeded or failed. + let _ = self + .conn + .xinput_grab_device( + self.window, + time, + // Allow the device to continue sending events + x11rb::protocol::xproto::GrabMode::ASYNC, + // Allow other devices to continue sending events. + x11rb::protocol::xproto::GrabMode::ASYNC, + // Doesn't work as documented, I have no idea. + true, + device.id, + &device.mask, + ) + .unwrap() + .reply() + .unwrap(); + } + } + fn parent_left(&mut self, master: u16, time: Timestamp) { + for device in &self.open_devices { + let Some(tool) = self.tool_infos.get_mut(&device.id) else { + continue; + }; + if master != tool.master { + continue; + } + // release and out, if need be. + if tool.phase == Phase::Down { + self.events.push(raw::Event::Tool { + tool: device.id, + event: raw::ToolEvent::Up, + }); + }; + if matches!(tool.phase, Phase::In | Phase::Down) { + self.events.push(raw::Event::Tool { + tool: device.id, + event: raw::ToolEvent::Out, + }); + }; + tool.phase = Phase::Out; + // Don't care if it succeeded or failed. + self.conn + .xinput_ungrab_device(time, device.id) + .unwrap() + .check() + .unwrap(); + } + } } impl super::PlatformImpl for Manager { @@ -1055,18 +1155,47 @@ impl super::PlatformImpl for Manager { while let Ok(Some(event)) = self.conn.poll_for_event() { use x11rb::protocol::Event; match event { + Event::XinputLeave(leave) => { + // MASTER POINTER ONLY. Cursor has left the client bounds. + self.parent_left(leave.deviceid, leave.time); + } + Event::XinputEnter(enter) => { + // MASTER POINTER ONLY. Cursor has entered client bounds. + self.parent_entered(enter.deviceid, enter.time); + } Event::XinputProximityIn(x) => { - // x,device_id is total garbage? what did I do to deserve this fate.- - self.events.push(raw::Event::Tool { - tool: *self.tool_infos.keys().next().unwrap(), - event: raw::ToolEvent::In { tablet: 0 }, - }); + // Not guaranteed to be sent, eg. if the tool comes in proximity while + // over a different window. We'll need to emulate the In event in such cases. + + // wh.. why.. + let device_id = x.device_id & 0x7f; + let Some(tool) = self.tool_infos.get_mut(&device_id) else { + continue; + }; + if tool.phase == Phase::Out { + tool.phase = Phase::In; + self.events.push(raw::Event::Tool { + tool: device_id, + event: raw::ToolEvent::In { tablet: 0 }, + }); + } } Event::XinputProximityOut(x) => { - self.events.push(raw::Event::Tool { - tool: *self.tool_infos.keys().next().unwrap(), - event: raw::ToolEvent::Out, - }); + // Not guaranteed to be sent, eg. if the tool leaves the window before going out. + // We'll need to emulate the In event in such cases. + + // wh.. why.. + let device_id = x.device_id & 0x7f; + let Some(tool) = self.tool_infos.get_mut(&device_id) else { + continue; + }; + if tool.phase != Phase::Out { + tool.phase = Phase::Out; + self.events.push(raw::Event::Tool { + tool: device_id, + event: raw::ToolEvent::Out, + }); + } } // XinputDeviceButtonPress, ButtonPress, XinputRawButtonPress are red herrings. // Dear X consortium... What the fuck? @@ -1137,7 +1266,14 @@ impl super::PlatformImpl for Manager { // Fetch it! m.axisvalues.get(usize::from(idx)).copied() }; - let tool_info = self.tool_infos.get(&device_id)?; + let tool_info = self.tool_infos.get_mut(&device_id)?; + if tool_info.phase == Phase::Out { + tool_info.phase = Phase::In; + self.events.push(raw::Event::Tool { + tool: device_id, + event: raw::ToolEvent::In { tablet: 0 }, + }); + } // Access valuators, and map them to our range for the associated axis. let pressure = tool_info .pressure @@ -1157,6 +1293,8 @@ impl super::PlatformImpl for Manager { tool: device_id, event: raw::ToolEvent::Pose(crate::axis::Pose { // Seems to already be in logical space. + // Using this seems to be the "wrong" solution. It's the master's position, + // which gets funky when two tools are active under the same master. position: [fixed16_to_f32(m.event_x), fixed16_to_f32(m.event_y)], distance: crate::util::NicheF32::NONE, pressure, @@ -1176,7 +1314,7 @@ impl super::PlatformImpl for Manager { Some(()) }; if try_uwu().is_none() { - println!("failed to fetch axes."); + //println!("failed to fetch axes."); } } Event::XinputDeviceValuator(m) => { @@ -1238,7 +1376,7 @@ impl super::PlatformImpl for Manager { }; let button_idx = u32::from(e.detail); - if button_idx == 0 || pad.total_buttons < button_idx { + if button_idx == 0 || button_idx > pad.total_buttons { // Okay, there's a weird off-by-one here, that even throws off the `xinput` debug // utility. My Intuos Pro S reports 11 buttons, but the maximum button index is.... 11, // which is clearly invalid. Silly. @@ -1263,8 +1401,7 @@ impl super::PlatformImpl for Manager { self.repopulate(); } } - other => println!("Other: {other:?}"), - //_ => (), + _ => (), } } Ok(()) From dede154597a8c50625689db1fb67d666332671b5 Mon Sep 17 00:00:00 2001 From: Aspen Pratt Date: Fri, 13 Sep 2024 18:29:10 -0700 Subject: [PATCH 09/15] xwayland fixes, frames, tablet associations. --- src/platform/xinput2/mod.rs | 422 +++++++++++++++++++++++------------- 1 file changed, 276 insertions(+), 146 deletions(-) diff --git a/src/platform/xinput2/mod.rs b/src/platform/xinput2/mod.rs index 9710d4a..764f24f 100644 --- a/src/platform/xinput2/mod.rs +++ b/src/platform/xinput2/mod.rs @@ -43,6 +43,8 @@ const EMULATED_TABLET_NAME: &str = "octotablet emulated"; const TYPE_MOUSE: &str = "MOUSE"; const TYPE_TOUCHSCREEN: &str = "TOUCHSCREEN"; +const DUMMY_TABLET_ID: u8 = 0; + /// Comes from `xinput_open_device`. Some APIs use u16. Confusing! pub type ID = u8; /// Comes from datasize of "button count" field of `ButtonInfo` - button names in xinput are indices, @@ -135,32 +137,36 @@ fn xwayland_type_from_name(device_name: &str) -> Option { } #[derive(Copy, Clone)] -enum ToolName<'a> { - NameOnly(&'a str), - NameAndId(&'a str, crate::tool::HardwareID), +struct ToolName<'a> { + /// The friendly form of the name, minus ID code. + human_readable: &'a str, + /// The tablet name we expect to own this tool + maybe_associated_tablet: Option<&'a str>, + /// The hardware serial of the tool. + id: Option, } impl<'a> ToolName<'a> { - fn name(self) -> &'a str { - match self { - Self::NameAndId(name, _) | Self::NameOnly(name) => name, - } + fn human_readable(self) -> &'a str { + self.human_readable } fn id(self) -> Option { - match self { - Self::NameAndId(_, id) => Some(id), - Self::NameOnly(_) => None, - } + self.id + } + fn maybe_associated_tablet(self) -> Option<&'a str> { + self.maybe_associated_tablet } } -/// From the user-facing Device name, try to parse a tool's hardware id. -fn tool_id_from_name(name: &str) -> ToolName { +/// From the user-facing Device name, try to parse several tool fields. +fn parse_tool_name(name: &str) -> ToolName { // X11 seems to place tool hardware IDs within the human-readable Name of the device, and this is // the only place it is exposed. Predictably, as with all things X, this is not documented as far - // as I can tell. From experience, it consists of the name, a space, and a hex number (or zero) - // in parentheses - This is a hueristic and likely non-exhaustive, Bleh. + // as I can tell. + + // From experiments, it consists of the [tablet name][tool type string][hex number (or zero) + // in parentheses] - This is a hueristic and likely non-exhaustive, for example it does not apply to xwayland. - let try_parse = || -> Option<(&str, crate::tool::HardwareID)> { + let try_parse_id = || -> Option<(&str, crate::tool::HardwareID)> { // Detect the range of characters within the last set of parens. let open_paren = name.rfind('(')?; let after_open_paren = open_paren + 1; @@ -187,12 +193,36 @@ fn tool_id_from_name(name: &str) -> ToolName { Some((name_text, crate::tool::HardwareID(id_num))) }; - if let Some((name, id)) = try_parse() { - ToolName::NameAndId(name, id) - } else { - ToolName::NameOnly(name) + let id_parse_result = try_parse_id(); + + let (human_readable, id) = match id_parse_result { + Some((name, id)) => (name, Some(id)), + None => (name, None), + }; + + let try_parse_maybe_associated_tablet = || -> Option<&str> { + // Hueristic, of course. These are the only two kinds of hardware I have to test with, + // unsure how e.g. an airbrush would register. + if let Some(tablet_name) = human_readable.strip_suffix(" Pen") { + return Some(tablet_name); + } + if let Some(tablet_name) = human_readable.strip_suffix(" Eraser") { + return Some(tablet_name); + } + None + }; + + ToolName { + human_readable, + maybe_associated_tablet: try_parse_maybe_associated_tablet(), + id, } } +fn pad_maybe_associated_tablet(name: &str) -> Option { + // Hueristic, of course. + name.strip_suffix(" Pad") + .map(|prefix| prefix.to_owned() + " Pen") +} /// Turn an xinput fixed-point number into a float, rounded. // I could probably keep them fixed for more maths, but this is easy for right now. fn fixed32_to_f32(fixed: xinput::Fp3232) -> f32 { @@ -244,7 +274,8 @@ enum Phase { Out, } -/// Contains the metadata for translating a device's events to octotablet events. +/// Contains the metadata for translating a device's events to octotablet events, +/// as well as the x11 specific state required to emulate certain events. struct ToolInfo { pressure: Option, tilt: [Option; 2], @@ -257,11 +288,19 @@ struct ToolInfo { phase: Phase, /// The master cursor. Grab this device when this cursor Enters, release it when it /// leaves. - master: u16, + master_pointer: u16, + /// The master keyboard associated with the master pointer. + master_keyboard: u16, + is_grabbed: bool, + // A change has occured on this pump that requires a frame event at this time. + // (pose, button, enter, ect) + frame_pending: Option, } struct PadInfo { ring: Option, + /// The tablet this tool belongs to, based on heuristics. + tablet: Option, } struct BitDiff { @@ -330,7 +369,6 @@ struct OpenDevice { pub struct Manager { conn: x11rb::rust_connection::RustConnection, - _xinput_minor_version: u16, tool_infos: std::collections::BTreeMap, open_devices: Vec, tools: Vec, @@ -341,6 +379,8 @@ pub struct Manager { window: x11rb::protocol::xproto::Window, atom_usb_id: Option>, atom_device_node: Option>, + // What is the most recent event timecode? + server_time: Timestamp, } impl Manager { @@ -352,15 +392,16 @@ impl Manager { conn.extension_information(xinput::X11_EXTENSION_NAME) .unwrap() .unwrap(); - let version = conn - // What the heck is "name"? it is totally undocumented and is not part of the XLib interface. - // I was unable to reverse engineer it, it seems to work regardless of what data is given to it. - .xinput_get_extension_version(b"Fixme!") - .unwrap() - .reply() - .unwrap(); + /*let version = conn + // What the heck is "name"? it is totally undocumented and is not part of the XLib interface. + // I was unable to reverse engineer it, it seems to work regardless of what data is given to it. + .xinput_get_extension_version(b"Fixme!") + .unwrap() + .reply() + .unwrap();*/ + let version = conn.xinput_xi_query_version(2, 2).unwrap().reply().unwrap(); - assert!(version.present && version.server_major >= 2); + assert!(version.major_version >= 2); // conn.xinput_select_extension_event( // window, @@ -390,98 +431,10 @@ impl Manager { .check() .unwrap(); - // Testing with itty four button guy, - // TABLET = "Wacom Intuos S Pen" - // PAD = "Wacom Intuos S Pad" - // STYLUS = "Wacom Intuos S Pen Pen (0x7802cf3)" (no that isn't a typo lmao) - - // Fetch existing devices. It is important to do this after we requested to recieve `DEVICE_CHANGED` events, - // lest we run into TOCTOU bugs! - /* - let mut interest = vec![]; - */ - - let devices = conn.xinput_list_input_devices().unwrap().reply().unwrap(); - let mut flat_infos = &devices.infos[..]; - for (name, device) in devices.names.iter().zip(devices.devices.iter()) { - let ty = if device.device_type != 0 { - let mut ty = conn - .get_atom_name(device.device_type) - .unwrap() - .reply() - .unwrap() - .name; - ty.push(0); - std::ffi::CString::from_vec_with_nul(ty).ok() - } else { - None - }; - if let Ok(name) = std::str::from_utf8(&name.name) { - println!("{} - {name}", device.device_id); - } else { - println!(""); - } - /*if ty.as_deref() == Some(TYPE_STYLUS) || ty.as_deref() == Some(TYPE_ERASER) { - println!("^^ Binding ^^"); - //let _open = conn - // .xinput_open_device(device.device_id) - // .unwrap() - // .reply() - // .unwrap(); - //interest.push(device.device_id); - }*/ - println!(" {ty:?} - {device:?}"); - // Take the infos for this device from the list. - let infos = { - let (head, tail) = flat_infos.split_at(usize::from(device.num_class_info)); - flat_infos = tail; - head - }; - - for info in infos { - println!(" * {info:?}"); - } - } - /* - let mut interest = interest - .into_iter() - .map(|id| { - xinput::EventMask { - deviceid: id.into(), - mask: [ - // Cursor entering and leaving client area - xinput::XIEventMask::ENTER - | xinput::XIEventMask::LEAVE - // Barrel and tip buttons - | xinput::XIEventMask::BUTTON_PRESS - | xinput::XIEventMask::BUTTON_RELEASE - // Axis movement - | xinput::XIEventMask::MOTION, - ] - .into(), - } - }) - .collect::>(); - interest.push(xinput::EventMask { - deviceid: 0, - mask: [ - // Barrel and tip buttons - // device add/remove/capabilities changed. - xinput::XIEventMask::HIERARCHY, - ] - .into(), - }); - - // Register with the server that we want to listen in on these events for all current devices: - conn.xinput_xi_select_events(window, &interest) - .unwrap() - .check() - .unwrap();*/ - // Future note for how to access core events, if needed. // "XSelectInput" is just a wrapper over this, funny! // https://github.com/mirror/libX11/blob/ff8706a5eae25b8bafce300527079f68a201d27f/src/SelInput.c#L33 - conn.change_window_attributes( + /*conn.change_window_attributes( window, &x11rb::protocol::xproto::ChangeWindowAttributesAux { event_mask: Some(x11rb::protocol::xproto::EventMask::NO_EVENT), @@ -490,7 +443,7 @@ impl Manager { ) .unwrap() .check() - .unwrap(); + .unwrap();*/ let atom_usb_id = conn .intern_atom(false, b"Device Product ID") @@ -505,7 +458,6 @@ impl Manager { let mut this = Self { conn, - _xinput_minor_version: version.server_minor, tool_infos: std::collections::BTreeMap::new(), pad_infos: std::collections::BTreeMap::new(), open_devices: vec![], @@ -516,6 +468,7 @@ impl Manager { window, atom_device_node, atom_usb_id, + server_time: 0, }; // Poll for devices. @@ -641,11 +594,24 @@ impl Manager { } // Try to parse the hardware ID from the name field. - let name_fields = raw_name.as_deref().map(tool_id_from_name); + let name_fields = raw_name.as_deref().map(parse_tool_name); + + let tablet_id = name_fields + .and_then(ToolName::maybe_associated_tablet) + .and_then(|expected| { + // Find the device with the expected name, and return it's ID if found. + let tablet_info = device_queries + .infos + .iter() + .find(|info| info.name == expected.as_bytes())?; + Some(tablet_info.deviceid.try_into().unwrap()) + }); let mut octotablet_info = crate::tool::Tool { internal_id: super::InternalID::XInput2(device.device_id), - name: name_fields.map(ToolName::name).map(ToOwned::to_owned), + name: name_fields + .map(ToolName::human_readable) + .map(ToOwned::to_owned), hardware_id: name_fields.and_then(ToolName::id), wacom_id: None, tool_type: Some(ty), @@ -656,9 +622,26 @@ impl Manager { pressure: None, tilt: [None, None], wheel: None, - tablet: None, + tablet: tablet_id, phase: Phase::Out, - master: query.attachment, + master_pointer: query.attachment, + master_keyboard: device_queries + .infos + .iter() + .find_map(|q| { + // Find the info for the master pointer + if q.deviceid == query.attachment { + // Look at the master pointer's attachment, + // which is the associated master keyboard's ID. + Some(q.attachment) + } else { + None + } + }) + // Above search should be infallible but I trust nothing at this point. + .unwrap_or_default(), + is_grabbed: false, + frame_pending: None, }; // Look for axes! @@ -793,6 +776,8 @@ impl Manager { if v.mode != xinput::ValuatorMode::ABSOLUTE { continue; } + // This fails to detect xwayland's Ring axis, since it is present but not labeled. + // However, in my testing, it's borked anyways and always returns position 71. let Some(label) = self .conn .get_atom_name(v.label) @@ -849,8 +834,27 @@ impl Manager { total_buttons: buttons.into(), groups: vec![group], }); - self.pad_infos - .insert(device.device_id, PadInfo { ring: ring_info }); + + // Find the tablet this belongs to. + let tablet = raw_name + .as_deref() + .and_then(pad_maybe_associated_tablet) + .and_then(|expected| { + // Find the device with the expected name, and return it's ID if found. + let tablet_info = device_queries + .infos + .iter() + .find(|info| info.name == expected.as_bytes())?; + Some(tablet_info.deviceid.try_into().unwrap()) + }); + + self.pad_infos.insert( + device.device_id, + PadInfo { + ring: ring_info, + tablet, + }, + ); } DeviceType::Tablet => { // Tablets are of... dubious usefulness in xinput? @@ -1025,7 +1029,50 @@ impl Manager { println!("Grab {} - {:?}", device.device_id, status);*/ } - if !self.tools.is_empty() { + // True if any tablet refers to a non-existant device. + let mut wants_dummy_tablet = false; + for tool in self.tool_infos.values_mut() { + // Look through associated tablet ids. If any refers to a non-existant device, refer + // instead to a dummy device. + if let Some(desired_tablet) = tool.tablet { + if !self + .tablets + .iter() + .any(|tablet| *tablet.internal_id.unwrap_xinput2() == desired_tablet) + { + tool.tablet = None; + wants_dummy_tablet = true; + } + } else { + wants_dummy_tablet = true; + } + } + for (id, pad) in &mut self.pad_infos { + // Look through associated tablet ids. If any refers to a non-existant device, refer + // instead to a dummy device. + if let Some(desired_tablet) = pad.tablet { + if !self + .tablets + .iter() + .any(|tablet| *tablet.internal_id.unwrap_xinput2() == desired_tablet) + { + pad.tablet = None; + wants_dummy_tablet = true; + } + } else { + wants_dummy_tablet = true; + } + + // In x11, pads cannot roam between tablets. Eagerly announce their attachment just once. + self.events.push(raw::Event::Pad { + pad: *id, + event: raw::PadEvent::Enter { + tablet: pad.tablet.unwrap_or(DUMMY_TABLET_ID), + }, + }); + } + + if wants_dummy_tablet { // So.... xinput doesn't have the same "Tablet owns pads and tools" // hierarchy as we do. When we associate tools with tablets, we need a tablet // to bind it to, but xinput does not necessarily provide one. @@ -1033,7 +1080,7 @@ impl Manager { // Wacom tablets and the DECO-01 use a consistent naming scheme, where tools are called // {Pen, Eraser} (hardware id), which we can use to extract such information. self.tablets.push(crate::tablet::Tablet { - internal_id: super::InternalID::XInput2(0), + internal_id: super::InternalID::XInput2(DUMMY_TABLET_ID), name: Some(EMULATED_TABLET_NAME.to_owned()), usb_id: None, }); @@ -1090,7 +1137,8 @@ impl Manager { let Some(tool) = self.tool_infos.get_mut(&device.id) else { continue; }; - if master != tool.master { + let is_child = tool.master_pointer == master || tool.master_keyboard == master; + if tool.is_grabbed || !is_child { continue; } @@ -1112,6 +1160,7 @@ impl Manager { .unwrap() .reply() .unwrap(); + tool.is_grabbed = true; } } fn parent_left(&mut self, master: u16, time: Timestamp) { @@ -1119,9 +1168,27 @@ impl Manager { let Some(tool) = self.tool_infos.get_mut(&device.id) else { continue; }; - if master != tool.master { + let is_child = tool.master_pointer == master || tool.master_keyboard == master; + if !tool.is_grabbed || !is_child { continue; } + + let was_in = matches!(tool.phase, Phase::In | Phase::Down); + + if was_in { + // Emit frame for previous events before sending more + if let Some(last_time) = tool.frame_pending.replace(time) { + if last_time != time { + self.events.push(raw::Event::Tool { + tool: device.id, + event: raw::ToolEvent::Frame(Some(crate::events::FrameTimestamp( + std::time::Duration::from_millis(last_time.into()), + ))), + }); + } + } + } + // release and out, if need be. if tool.phase == Phase::Down { self.events.push(raw::Event::Tool { @@ -1129,7 +1196,7 @@ impl Manager { event: raw::ToolEvent::Up, }); }; - if matches!(tool.phase, Phase::In | Phase::Down) { + if was_in { self.events.push(raw::Event::Tool { tool: device.id, event: raw::ToolEvent::Out, @@ -1142,6 +1209,7 @@ impl Manager { .unwrap() .check() .unwrap(); + tool.is_grabbed = false; } } } @@ -1155,18 +1223,26 @@ impl super::PlatformImpl for Manager { while let Ok(Some(event)) = self.conn.poll_for_event() { use x11rb::protocol::Event; match event { - Event::XinputLeave(leave) => { + // xwayland fails to emit Leave/Enter when the cursor is warped to/from another window + // by a proximity in event. However, it emits a FocusOut/FocusIn for the associated + // master keyboard in that case, which we can use to emulate. + // On a genuine X11 server this causes the device release logic to happen twice. + // Could we just always rely on FocusOut, or would that add more edge cases? + Event::XinputLeave(leave) | Event::XinputFocusOut(leave) => { // MASTER POINTER ONLY. Cursor has left the client bounds. self.parent_left(leave.deviceid, leave.time); + self.server_time = leave.time; } - Event::XinputEnter(enter) => { + Event::XinputEnter(enter) | Event::XinputFocusIn(enter) => { // MASTER POINTER ONLY. Cursor has entered client bounds. self.parent_entered(enter.deviceid, enter.time); + self.server_time = enter.time; } + // Proximity (coming in and out of sense range) events. + // Not guaranteed to be sent, eg. if the tool comes in proximity while + // over a different window. We'll need to emulate the In event in such cases. + // Never sent on xwayland. Event::XinputProximityIn(x) => { - // Not guaranteed to be sent, eg. if the tool comes in proximity while - // over a different window. We'll need to emulate the In event in such cases. - // wh.. why.. let device_id = x.device_id & 0x7f; let Some(tool) = self.tool_infos.get_mut(&device_id) else { @@ -1176,15 +1252,14 @@ impl super::PlatformImpl for Manager { tool.phase = Phase::In; self.events.push(raw::Event::Tool { tool: device_id, - event: raw::ToolEvent::In { tablet: 0 }, + event: raw::ToolEvent::In { + tablet: tool.tablet.unwrap_or(DUMMY_TABLET_ID), + }, }); } + self.server_time = x.time; } Event::XinputProximityOut(x) => { - // Not guaranteed to be sent, eg. if the tool leaves the window before going out. - // We'll need to emulate the In event in such cases. - - // wh.. why.. let device_id = x.device_id & 0x7f; let Some(tool) = self.tool_infos.get_mut(&device_id) else { continue; @@ -1196,6 +1271,7 @@ impl super::PlatformImpl for Manager { event: raw::ToolEvent::Out, }); } + self.server_time = x.time; } // XinputDeviceButtonPress, ButtonPress, XinputRawButtonPress are red herrings. // Dear X consortium... What the fuck? @@ -1246,6 +1322,7 @@ impl super::PlatformImpl for Manager { }); } } + self.server_time = e.time; } Event::XinputMotion(m) => { // Tool valuators. @@ -1267,11 +1344,28 @@ impl super::PlatformImpl for Manager { m.axisvalues.get(usize::from(idx)).copied() }; let tool_info = self.tool_infos.get_mut(&device_id)?; + + // About to emit events. Emit frame if the time differs. + if let Some(last_time) = tool_info.frame_pending.replace(m.time) { + if last_time != m.time { + self.events.push(raw::Event::Tool { + tool: device_id, + event: raw::ToolEvent::Frame(Some( + crate::events::FrameTimestamp( + std::time::Duration::from_millis(last_time.into()), + ), + )), + }); + } + } + if tool_info.phase == Phase::Out { tool_info.phase = Phase::In; self.events.push(raw::Event::Tool { tool: device_id, - event: raw::ToolEvent::In { tablet: 0 }, + event: raw::ToolEvent::In { + tablet: tool_info.tablet.unwrap_or(DUMMY_TABLET_ID), + }, }); } // Access valuators, and map them to our range for the associated axis. @@ -1316,13 +1410,14 @@ impl super::PlatformImpl for Manager { if try_uwu().is_none() { //println!("failed to fetch axes."); } + self.server_time = m.time; } Event::XinputDeviceValuator(m) => { // Pad valuators. Instead of the arbtrary number of valuators that the tools // are sent, this sends in groups of six. Ignore all of them except the packet that // contains our ring value. - if let Some(pad_info) = self.pad_infos.get(&m.device_id) { + if let Some(pad_info) = self.pad_infos.get_mut(&m.device_id) { let Some(ring_info) = pad_info.ring else { continue; }; @@ -1351,6 +1446,7 @@ impl super::PlatformImpl for Manager { continue; } + // About to emit events. Emit frame if the time differs. self.events.push(raw::Event::Pad { pad: m.device_id, event: raw::PadEvent::Group { @@ -1363,6 +1459,25 @@ impl super::PlatformImpl for Manager { }, }, }); + // Weirdly, this event is the only one without a timestamp. + // So, we track the current time in all the other events, and can + // guestimate based on that. + self.events.push(raw::Event::Pad { + pad: m.device_id, + event: raw::PadEvent::Group { + group: m.device_id, + event: raw::PadGroupEvent::Ring { + ring: m.device_id, + event: crate::events::TouchStripEvent::Frame(Some( + crate::events::FrameTimestamp( + std::time::Duration::from_millis( + self.server_time.into(), + ), + ), + )), + }, + }, + }); } } Event::XinputDeviceButtonPress(e) | Event::XinputDeviceButtonRelease(e) => { @@ -1392,18 +1507,33 @@ impl super::PlatformImpl for Manager { pressed: e.response_type == 69, }, }); + self.server_time = e.time; } - Event::XinputHierarchy(_) => { + Event::XinputHierarchy(h) => { // The event does not necessarily reflect *all* changes, the spec specifically says // that the client should probably just rescan. lol if !has_repopulated { has_repopulated = true; self.repopulate(); } + self.server_time = h.time; } _ => (), } } + + // Emit pending frames. + for (id, tool) in &mut self.tool_infos { + if let Some(time) = tool.frame_pending.take() { + self.events.push(raw::Event::Tool { + tool: *id, + event: raw::ToolEvent::Frame(Some(crate::events::FrameTimestamp( + std::time::Duration::from_millis(time.into()), + ))), + }); + } + } + Ok(()) } fn raw_events(&self) -> super::RawEventsIter<'_> { From fdda160b9688aee7f7823db546da12a455391e2c Mon Sep 17 00:00:00 2001 From: Aspen Pratt Date: Sat, 14 Sep 2024 16:07:27 -0700 Subject: [PATCH 10/15] generational IDs --- src/platform/xinput2/mod.rs | 454 +++++++++++++++++++++--------------- 1 file changed, 268 insertions(+), 186 deletions(-) diff --git a/src/platform/xinput2/mod.rs b/src/platform/xinput2/mod.rs index 764f24f..a1d2c72 100644 --- a/src/platform/xinput2/mod.rs +++ b/src/platform/xinput2/mod.rs @@ -7,9 +7,7 @@ use x11rb::{ }, }; -// Note: Device Product ID (286) property of tablets states USB vid and pid, and Device Node (285) -// lists path. Impl that pleas. thanks Uwu - +// Some necessary constants not defined by x11rb: const XI_ALL_DEVICES: u16 = 0; const XI_ALL_MASTER_DEVICES: u8 = 1; /// Magic timestamp signalling to the server "now". @@ -43,10 +41,6 @@ const EMULATED_TABLET_NAME: &str = "octotablet emulated"; const TYPE_MOUSE: &str = "MOUSE"; const TYPE_TOUCHSCREEN: &str = "TOUCHSCREEN"; -const DUMMY_TABLET_ID: u8 = 0; - -/// Comes from `xinput_open_device`. Some APIs use u16. Confusing! -pub type ID = u8; /// Comes from datasize of "button count" field of `ButtonInfo` - button names in xinput are indices, /// with the zeroth index referring to the tool "down" state. pub type ButtonID = std::num::NonZero; @@ -136,6 +130,21 @@ fn xwayland_type_from_name(device_name: &str) -> Option { }) } +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub(super) enum ID { + /// Special value for the emulated tablet. This is an invalid ID for tools and pads. + /// A bit of an API design whoopsie! + EmulatedTablet, + ID { + /// Xinput re-uses the IDs of removed devices. + /// Since we need to keep around devices for an extra frame to report added/removed, + /// it means a conflict can occur. + generation: u8, + /// XI1 ID. XI2 uses u16 but i'm just generally confused UwU + device_id: std::num::NonZero, + }, +} + #[derive(Copy, Clone)] struct ToolName<'a> { /// The friendly form of the name, minus ID code. @@ -282,9 +291,9 @@ struct ToolInfo { wheel: Option, /// The tablet this tool belongs to, based on heuristics. /// When "In" is fired, this is the device to reference, because X doesn't provide - /// such info. If none, uses a dummy tablet. + /// such info. /// (tool -> tablet relationship is one-to-one-or-less in xinput instead of one-to-one-or-more as we expect) - tablet: Option, + tablet: ID, phase: Phase, /// The master cursor. Grab this device when this cursor Enters, release it when it /// leaves. @@ -299,72 +308,56 @@ struct ToolInfo { struct PadInfo { ring: Option, - /// The tablet this tool belongs to, based on heuristics. - tablet: Option, + /// The tablet this tool belongs to, based on heuristics, or Dummy. + tablet: ID, } -struct BitDiff { - bit_index: usize, - set: bool, +struct OpenDevice { + mask: Vec, + id: u8, } -struct BitDifferenceIter<'a> { - from: &'a [u32], - to: &'a [u32], - // cursor position: - // Which bit within the u32? - next_bit_idx: u32, - // Which word within the array? - cur_word: usize, -} -impl<'a> BitDifferenceIter<'a> { - fn diff(from: &'a [u32], to: &'a [u32]) -> Self { - Self { - from, - to, - next_bit_idx: 0, - cur_word: 0, - } - } -} -impl Iterator for BitDifferenceIter<'_> { - type Item = BitDiff; - fn next(&mut self) -> Option { - loop { - let to = self.to.get(self.cur_word)?; - let from = self.from.get(self.cur_word).copied().unwrap_or_default(); - let diff = to ^ from; - - // find the lowest set bit in the difference word. - let next_diff_idx = self.next_bit_idx + (diff >> self.next_bit_idx).trailing_zeros(); - - if next_diff_idx >= u32::BITS - 1 { - // Advance to the next word for next go around. - self.next_bit_idx = 0; - self.cur_word += 1; - } else { - // advance cursor regularly. - self.next_bit_idx = next_diff_idx + 1; - } - if next_diff_idx >= u32::BITS { - // No bit was set in this word, skip to next word. - continue; - } +fn tool_info_mut_from_device_id( + id: u8, + infos: &mut std::collections::BTreeMap, + now_generation: u8, +) -> Option<(ID, &mut ToolInfo)> { + let non_zero_id = std::num::NonZero::::new(id)?; + let id = ID::ID { + generation: now_generation, + device_id: non_zero_id, + }; - // Check what the difference we just found was. - let became_set = (to >> next_diff_idx) & 1 == 1; + infos.get_mut(&id).map(|info| (id, info)) +} +fn pad_info_mut_from_device_id( + id: u8, + infos: &mut std::collections::BTreeMap, + now_generation: u8, +) -> Option<(ID, &mut PadInfo)> { + let non_zero_id = std::num::NonZero::::new(id)?; + let id = ID::ID { + generation: now_generation, + device_id: non_zero_id, + }; - return Some(BitDiff { - bit_index: self.cur_word * u32::BITS as usize + next_diff_idx as usize, - set: became_set, - }); - } - } + infos.get_mut(&id).map(|info| (id, info)) } +fn pad_mut_from_device_id( + id: u8, + infos: &mut [crate::pad::Pad], + now_generation: u8, +) -> Option<(ID, &mut crate::pad::Pad)> { + let non_zero_id = std::num::NonZero::::new(id)?; + let id = ID::ID { + generation: now_generation, + device_id: non_zero_id, + }; -struct OpenDevice { - mask: Vec, - id: ID, + infos + .iter_mut() + .find(|pad| *pad.internal_id.unwrap_xinput2() == id) + .map(|info| (id, info)) } pub struct Manager { @@ -381,6 +374,8 @@ pub struct Manager { atom_device_node: Option>, // What is the most recent event timecode? server_time: Timestamp, + /// Device ID generation. Increment when one or more devices is removed in a frame. + device_generation: u8, } impl Manager { @@ -469,6 +464,7 @@ impl Manager { atom_device_node, atom_usb_id, server_time: 0, + device_generation: 0, }; // Poll for devices. @@ -483,15 +479,18 @@ impl Manager { // report adds/removes. self.tools.clear(); self.tablets.clear(); + self.pads.clear(); self.tool_infos.clear(); self.pad_infos.clear(); + if !self.open_devices.is_empty() { + self.device_generation = self.device_generation.wrapping_add(1); + } + for device in self.open_devices.drain(..) { - self.conn - .xinput_close_device(device.id) - .unwrap() - .check() - .unwrap(); + // Don't care if the effects of closure went through, just + // that it's sent. Some may fail. Fixme! + let _ = self.conn.xinput_close_device(device.id).unwrap(); } // Tools ids to bulk-enable events on. let mut tool_listen_events = vec![XI_ALL_MASTER_DEVICES]; @@ -525,6 +524,13 @@ impl Manager { .into_iter() .zip(device_list.devices.into_iter()) { + // Zero is a special value (ALL_DEVICES), and can't be used by a device. + let nonzero_id = std::num::NonZero::::new(device.device_id).unwrap(); + let octotablet_id = ID::ID { + generation: self.device_generation, + device_id: nonzero_id, + }; + let _infos = { // Split off however many axes this device claims. let (infos, tail_infos) = flat_infos.split_at(device.num_class_info.into()); @@ -604,11 +610,17 @@ impl Manager { .infos .iter() .find(|info| info.name == expected.as_bytes())?; - Some(tablet_info.deviceid.try_into().unwrap()) + + let id = u8::try_from(tablet_info.deviceid).ok()?; + Some(ID::ID { + generation: self.device_generation, + // 0 is a special value, this is infallible. + device_id: id.try_into().unwrap(), + }) }); let mut octotablet_info = crate::tool::Tool { - internal_id: super::InternalID::XInput2(device.device_id), + internal_id: super::InternalID::XInput2(octotablet_id), name: name_fields .map(ToolName::human_readable) .map(ToOwned::to_owned), @@ -622,7 +634,7 @@ impl Manager { pressure: None, tilt: [None, None], wheel: None, - tablet: tablet_id, + tablet: tablet_id.unwrap_or(ID::EmulatedTablet), phase: Phase::Out, master_pointer: query.attachment, master_keyboard: device_queries @@ -761,7 +773,7 @@ impl Manager { tool_listen_events.push(device.device_id); self.tools.push(octotablet_info); - self.tool_infos.insert(device.device_id, x11_info); + self.tool_infos.insert(octotablet_id, x11_info); } DeviceType::Pad => { let mut buttons = 0; @@ -816,7 +828,7 @@ impl Manager { if ring_info.is_some() { rings.push(crate::pad::Ring { granularity: None, - internal_id: crate::platform::InternalID::XInput2(device.device_id), + internal_id: crate::platform::InternalID::XInput2(octotablet_id), }); }; // X11 has no concept of groups (i don't .. think?) @@ -824,13 +836,13 @@ impl Manager { let group = crate::pad::Group { buttons: (0..buttons).map(Into::into).collect::>(), feedback: None, - internal_id: crate::platform::InternalID::XInput2(device.device_id), + internal_id: crate::platform::InternalID::XInput2(octotablet_id), mode_count: None, rings, strips: vec![], }; self.pads.push(crate::pad::Pad { - internal_id: crate::platform::InternalID::XInput2(device.device_id), + internal_id: crate::platform::InternalID::XInput2(octotablet_id), total_buttons: buttons.into(), groups: vec![group], }); @@ -845,14 +857,19 @@ impl Manager { .infos .iter() .find(|info| info.name == expected.as_bytes())?; - Some(tablet_info.deviceid.try_into().unwrap()) + let id = u8::try_from(tablet_info.deviceid).ok()?; + Some(ID::ID { + generation: self.device_generation, + // 0 is ALL_DEVICES, this is infallible. + device_id: id.try_into().unwrap(), + }) }); self.pad_infos.insert( - device.device_id, + octotablet_id, PadInfo { ring: ring_info, - tablet, + tablet: tablet.unwrap_or(ID::EmulatedTablet), }, ); } @@ -896,7 +913,7 @@ impl Manager { // We can also fetch device path here. let tablet = crate::tablet::Tablet { - internal_id: super::InternalID::XInput2(device.device_id), + internal_id: super::InternalID::XInput2(octotablet_id), name: raw_name, usb_id, }; @@ -1034,41 +1051,56 @@ impl Manager { for tool in self.tool_infos.values_mut() { // Look through associated tablet ids. If any refers to a non-existant device, refer // instead to a dummy device. - if let Some(desired_tablet) = tool.tablet { + if let ID::ID { + device_id: desired_tablet, + .. + } = tool.tablet + { if !self .tablets .iter() - .any(|tablet| *tablet.internal_id.unwrap_xinput2() == desired_tablet) + .any(|tablet| match *tablet.internal_id.unwrap_xinput2() { + ID::ID { device_id, .. } => device_id == desired_tablet, + _ => false, + }) { - tool.tablet = None; - wants_dummy_tablet = true; + tool.tablet = ID::EmulatedTablet; } - } else { + } + + if tool.tablet == ID::EmulatedTablet { wants_dummy_tablet = true; } } for (id, pad) in &mut self.pad_infos { // Look through associated tablet ids. If any refers to a non-existant device, refer // instead to a dummy device. - if let Some(desired_tablet) = pad.tablet { + if let ID::ID { + device_id: desired_tablet, + .. + } = pad.tablet + { if !self .tablets .iter() - .any(|tablet| *tablet.internal_id.unwrap_xinput2() == desired_tablet) + .any(|tablet| match *tablet.internal_id.unwrap_xinput2() { + ID::ID { device_id, .. } => device_id == desired_tablet, + _ => false, + }) { - pad.tablet = None; wants_dummy_tablet = true; } - } else { + } + + if pad.tablet == ID::EmulatedTablet { wants_dummy_tablet = true; } // In x11, pads cannot roam between tablets. Eagerly announce their attachment just once. + // FIXME: on initial device enumeration these are lost due to `events.clear()` in `pump`. self.events.push(raw::Event::Pad { pad: *id, - event: raw::PadEvent::Enter { - tablet: pad.tablet.unwrap_or(DUMMY_TABLET_ID), - }, + event: raw::PadEvent::Enter { tablet: pad.tablet }, }); } @@ -1080,7 +1112,7 @@ impl Manager { // Wacom tablets and the DECO-01 use a consistent naming scheme, where tools are called // {Pen, Eraser} (hardware id), which we can use to extract such information. self.tablets.push(crate::tablet::Tablet { - internal_id: super::InternalID::XInput2(DUMMY_TABLET_ID), + internal_id: super::InternalID::XInput2(ID::EmulatedTablet), name: Some(EMULATED_TABLET_NAME.to_owned()), usb_id: None, }); @@ -1118,7 +1150,7 @@ impl Manager { // | xinput::XIEventMask::PROPERTY // Sent when a master device is bound, and the device controlling it // changes (thus presenting a master with different classes) - // We don't care about master devices, though! + // We don't listen for valuators nor buttons on master devices, though! // | xinput::XIEventMask::DEVICE_CHANGED ] .into(), @@ -1134,7 +1166,11 @@ impl Manager { } fn parent_entered(&mut self, master: u16, time: Timestamp) { for device in &self.open_devices { - let Some(tool) = self.tool_infos.get_mut(&device.id) else { + let Some((_, tool)) = tool_info_mut_from_device_id( + device.id, + &mut self.tool_infos, + self.device_generation, + ) else { continue; }; let is_child = tool.master_pointer == master || tool.master_keyboard == master; @@ -1165,7 +1201,11 @@ impl Manager { } fn parent_left(&mut self, master: u16, time: Timestamp) { for device in &self.open_devices { - let Some(tool) = self.tool_infos.get_mut(&device.id) else { + let Some((id, tool)) = tool_info_mut_from_device_id( + device.id, + &mut self.tool_infos, + self.device_generation, + ) else { continue; }; let is_child = tool.master_pointer == master || tool.master_keyboard == master; @@ -1180,7 +1220,7 @@ impl Manager { if let Some(last_time) = tool.frame_pending.replace(time) { if last_time != time { self.events.push(raw::Event::Tool { - tool: device.id, + tool: id, event: raw::ToolEvent::Frame(Some(crate::events::FrameTimestamp( std::time::Duration::from_millis(last_time.into()), ))), @@ -1192,13 +1232,13 @@ impl Manager { // release and out, if need be. if tool.phase == Phase::Down { self.events.push(raw::Event::Tool { - tool: device.id, + tool: id, event: raw::ToolEvent::Up, }); }; if was_in { self.events.push(raw::Event::Tool { - tool: device.id, + tool: id, event: raw::ToolEvent::Out, }); }; @@ -1245,15 +1285,19 @@ impl super::PlatformImpl for Manager { Event::XinputProximityIn(x) => { // wh.. why.. let device_id = x.device_id & 0x7f; - let Some(tool) = self.tool_infos.get_mut(&device_id) else { + let Some((id, tool)) = tool_info_mut_from_device_id( + device_id, + &mut self.tool_infos, + self.device_generation, + ) else { continue; }; if tool.phase == Phase::Out { tool.phase = Phase::In; self.events.push(raw::Event::Tool { - tool: device_id, + tool: id, event: raw::ToolEvent::In { - tablet: tool.tablet.unwrap_or(DUMMY_TABLET_ID), + tablet: tool.tablet, }, }); } @@ -1261,13 +1305,24 @@ impl super::PlatformImpl for Manager { } Event::XinputProximityOut(x) => { let device_id = x.device_id & 0x7f; - let Some(tool) = self.tool_infos.get_mut(&device_id) else { + let Some((id, tool)) = tool_info_mut_from_device_id( + device_id, + &mut self.tool_infos, + self.device_generation, + ) else { continue; }; + // Emulate Up before out if need be. + if tool.phase == Phase::Down { + self.events.push(raw::Event::Tool { + tool: id, + event: raw::ToolEvent::Up, + }); + } if tool.phase != Phase::Out { tool.phase = Phase::Out; self.events.push(raw::Event::Tool { - tool: device_id, + tool: id, event: raw::ToolEvent::Out, }); } @@ -1284,12 +1339,27 @@ impl super::PlatformImpl for Manager { continue; } let device_id = u8::try_from(e.deviceid).unwrap(); - if !self.tool_infos.contains_key(&device_id) { + let Some((id, tool)) = tool_info_mut_from_device_id( + device_id, + &mut self.tool_infos, + self.device_generation, + ) else { continue; }; let button_idx = u16::try_from(e.detail).unwrap(); + // Emulate In event if currently out. + if tool.phase == Phase::Out { + self.events.push(raw::Event::Tool { + tool: id, + event: raw::ToolEvent::In { + tablet: tool.tablet, + }, + }); + tool.phase = Phase::Up; + } + // Detail gives the "button index". match button_idx { // Doesn't occur, I don't think. @@ -1297,13 +1367,21 @@ impl super::PlatformImpl for Manager { // Tip button 1 => { if e.event_type == xinput::BUTTON_PRESS_EVENT { + if tool.phase == Phase::Down { + continue; + } + tool.phase = Phase::Down; self.events.push(raw::Event::Tool { - tool: device_id, + tool: id, event: raw::ToolEvent::Down, }); } else { + if tool.phase == Phase::Up { + continue; + } + tool.phase = Phase::Up; self.events.push(raw::Event::Tool { - tool: device_id, + tool: id, event: raw::ToolEvent::Up, }); } @@ -1311,7 +1389,7 @@ impl super::PlatformImpl for Manager { // Other (barrel) button. _ => { self.events.push(raw::Event::Tool { - tool: device_id, + tool: id, event: raw::ToolEvent::Button { button_id: crate::platform::ButtonID::XInput2( // Already checked != 0 @@ -1327,8 +1405,6 @@ impl super::PlatformImpl for Manager { Event::XinputMotion(m) => { // Tool valuators. let mut try_uwu = || -> Option<()> { - let device_id = m.deviceid.try_into().ok()?; - let valuator_fetch = |idx: u16| -> Option { // Check that it's not masked out- let word_idx = idx / u32::BITS as u16; @@ -1343,13 +1419,19 @@ impl super::PlatformImpl for Manager { // Fetch it! m.axisvalues.get(usize::from(idx)).copied() }; - let tool_info = self.tool_infos.get_mut(&device_id)?; + + let device_id = m.deviceid.try_into().ok()?; + let (id, tool) = tool_info_mut_from_device_id( + device_id, + &mut self.tool_infos, + self.device_generation, + )?; // About to emit events. Emit frame if the time differs. - if let Some(last_time) = tool_info.frame_pending.replace(m.time) { + if let Some(last_time) = tool.frame_pending.replace(m.time) { if last_time != m.time { self.events.push(raw::Event::Tool { - tool: device_id, + tool: id, event: raw::ToolEvent::Frame(Some( crate::events::FrameTimestamp( std::time::Duration::from_millis(last_time.into()), @@ -1359,32 +1441,32 @@ impl super::PlatformImpl for Manager { } } - if tool_info.phase == Phase::Out { - tool_info.phase = Phase::In; + if tool.phase == Phase::Out { + tool.phase = Phase::In; self.events.push(raw::Event::Tool { - tool: device_id, + tool: id, event: raw::ToolEvent::In { - tablet: tool_info.tablet.unwrap_or(DUMMY_TABLET_ID), + tablet: tool.tablet, }, }); } // Access valuators, and map them to our range for the associated axis. - let pressure = tool_info + let pressure = tool .pressure .and_then(|axis| { Some(axis.transform.transform_fixed(valuator_fetch(axis.index)?)) }) .and_then(crate::util::NicheF32::new_some) .unwrap_or(crate::util::NicheF32::NONE); - let tilt_x = tool_info.tilt[0].and_then(|axis| { + let tilt_x = tool.tilt[0].and_then(|axis| { Some(axis.transform.transform_fixed(valuator_fetch(axis.index)?)) }); - let tilt_y = tool_info.tilt[1].and_then(|axis| { + let tilt_y = tool.tilt[1].and_then(|axis| { Some(axis.transform.transform_fixed(valuator_fetch(axis.index)?)) }); self.events.push(raw::Event::Tool { - tool: device_id, + tool: id, event: raw::ToolEvent::Pose(crate::axis::Pose { // Seems to already be in logical space. // Using this seems to be the "wrong" solution. It's the master's position, @@ -1417,81 +1499,81 @@ impl super::PlatformImpl for Manager { // are sent, this sends in groups of six. Ignore all of them except the packet that // contains our ring value. - if let Some(pad_info) = self.pad_infos.get_mut(&m.device_id) { - let Some(ring_info) = pad_info.ring else { - continue; - }; - let absolute_ring_index = ring_info.index; - let Some(relative_ring_indox) = - absolute_ring_index.checked_sub(u16::from(m.first_valuator)) - else { - continue; - }; - if relative_ring_indox >= m.num_valuators.into() { - continue; - } + let Some((id, pad_info)) = pad_info_mut_from_device_id( + m.device_id, + &mut self.pad_infos, + self.device_generation, + ) else { + continue; + }; + let Some(ring_info) = pad_info.ring else { + continue; + }; + let absolute_ring_index = ring_info.index; + let Some(relative_ring_indox) = + absolute_ring_index.checked_sub(u16::from(m.first_valuator)) + else { + continue; + }; + if relative_ring_indox >= m.num_valuators.into() { + continue; + } - let Some(&valuator_value) = - m.valuators.get(usize::from(relative_ring_indox)) - else { - continue; - }; + let Some(&valuator_value) = m.valuators.get(usize::from(relative_ring_indox)) + else { + continue; + }; - if valuator_value == 0 { - // On release, this is snapped back to zero, but zero is also a valid value. There does not - // seem to be a method of checking when the interaction ended to avoid this. + if valuator_value == 0 { + // On release, this is snapped back to zero, but zero is also a valid value. There does not + // seem to be a method of checking when the interaction ended to avoid this. - // Snapping back to zero makes this entirely useless for knob control (which is the primary - // purpose of the ring) so we take this little loss. - continue; - } + // Snapping back to zero makes this entirely useless for knob control (which is the primary + // purpose of the ring) so we take this little loss. + continue; + } - // About to emit events. Emit frame if the time differs. - self.events.push(raw::Event::Pad { - pad: m.device_id, - event: raw::PadEvent::Group { - group: m.device_id, - event: raw::PadGroupEvent::Ring { - ring: m.device_id, - event: crate::events::TouchStripEvent::Pose( - ring_info.transform.transform(valuator_value as f32), - ), - }, + // About to emit events. Emit frame if the time differs. + self.events.push(raw::Event::Pad { + pad: id, + event: raw::PadEvent::Group { + group: id, + event: raw::PadGroupEvent::Ring { + ring: id, + event: crate::events::TouchStripEvent::Pose( + ring_info.transform.transform(valuator_value as f32), + ), }, - }); - // Weirdly, this event is the only one without a timestamp. - // So, we track the current time in all the other events, and can - // guestimate based on that. - self.events.push(raw::Event::Pad { - pad: m.device_id, - event: raw::PadEvent::Group { - group: m.device_id, - event: raw::PadGroupEvent::Ring { - ring: m.device_id, - event: crate::events::TouchStripEvent::Frame(Some( - crate::events::FrameTimestamp( - std::time::Duration::from_millis( - self.server_time.into(), - ), - ), - )), - }, + }, + }); + // Weirdly, this event is the only one without a timestamp. + // So, we track the current time in all the other events, and can + // guestimate based on that. + self.events.push(raw::Event::Pad { + pad: id, + event: raw::PadEvent::Group { + group: id, + event: raw::PadGroupEvent::Ring { + ring: id, + event: crate::events::TouchStripEvent::Frame(Some( + crate::events::FrameTimestamp( + std::time::Duration::from_millis(self.server_time.into()), + ), + )), }, - }); - } + }, + }); } Event::XinputDeviceButtonPress(e) | Event::XinputDeviceButtonRelease(e) => { // Pad buttons. - let Some(pad) = self - .pads - .iter() - .find(|pad| *pad.internal_id.unwrap_xinput2() == e.device_id) + let Some((id, pad_info)) = + pad_mut_from_device_id(e.device_id, &mut self.pads, self.device_generation) else { continue; }; let button_idx = u32::from(e.detail); - if button_idx == 0 || button_idx > pad.total_buttons { + if button_idx == 0 || button_idx > pad_info.total_buttons { // Okay, there's a weird off-by-one here, that even throws off the `xinput` debug // utility. My Intuos Pro S reports 11 buttons, but the maximum button index is.... 11, // which is clearly invalid. Silly. @@ -1500,7 +1582,7 @@ impl super::PlatformImpl for Manager { } self.events.push(raw::Event::Pad { - pad: e.device_id, + pad: id, event: raw::PadEvent::Button { // Shift 1-based to 0-based indexing. button_idx: button_idx - 1, From 2274bc745c30e6b244d46565db7f93acdd86cdfd Mon Sep 17 00:00:00 2001 From: Aspen Pratt Date: Sun, 15 Sep 2024 12:08:43 -0700 Subject: [PATCH 11/15] Timeout-based out events + refactor + remove enum_dispatch --- Cargo.lock | 13 -- Cargo.toml | 1 - src/platform/mod.rs | 69 ++++++- src/platform/xinput2/mod.rs | 364 +++++++++++++++++++++++------------- 4 files changed, 302 insertions(+), 145 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b1ce34f..e8d7bc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1043,18 +1043,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "enum_dispatch" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f33313078bb8d4d05a2733a94ac4c2d8a0df9a2b84424ebf4f33bfc224a890e" -dependencies = [ - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.52", -] - [[package]] name = "enumflags2" version = "0.7.9" @@ -2020,7 +2008,6 @@ dependencies = [ "bitflags 2.4.2", "cfg_aliases 0.2.0", "eframe", - "enum_dispatch", "raw-window-handle 0.5.2", "raw-window-handle 0.6.0", "sdl2", diff --git a/Cargo.toml b/Cargo.toml index 0f4d1fb..0d0470e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,6 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] # Common deps. bitflags = "2.4.2" -enum_dispatch = "0.3.12" raw-window-handle = "0.6.0" strum = { version = "0.26.2", features = ["derive"] } thiserror = "1.0.58" diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 85b3b05..bb2c38e 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -200,6 +200,8 @@ pub(crate) enum RawEventsIter<'a> { XInput2(std::slice::Iter<'a, crate::events::raw::Event>), #[cfg(ink_rts)] Ink(std::slice::Iter<'a, crate::events::raw::Event>), + // Prevent error when no backends are available. + Uninhabited(&'a std::convert::Infallible), } impl Iterator for RawEventsIter<'_> { type Item = crate::events::raw::Event; @@ -212,12 +214,12 @@ impl Iterator for RawEventsIter<'_> { Self::XInput2(xi) => xi.next().cloned().map(crate::events::raw::Event::id_into), #[cfg(ink_rts)] Self::Ink(ink) => ink.next().cloned().map(crate::events::raw::Event::id_into), + Self::Uninhabited(&u) => match u {}, } } } /// Trait that all platforms implement, giving the main `Manager` higher-level access to the black box. -#[enum_dispatch::enum_dispatch] pub(crate) trait PlatformImpl { #[allow(clippy::missing_errors_doc)] fn pump(&mut self) -> Result<(), crate::PumpError>; @@ -236,7 +238,6 @@ pub(crate) trait PlatformImpl { /// Static dispatch between compiled backends. /// Enum cause why not, in some cases this has one variant and is thus compiles away to the inner type transparently. /// Even empty enum is OK, since everything involving it becomes essentially `match ! {}` which is sound :D -#[enum_dispatch::enum_dispatch(PlatformImpl)] pub(crate) enum PlatformManager { #[cfg(wl_tablet)] Wayland(wl::Manager), @@ -245,3 +246,67 @@ pub(crate) enum PlatformManager { #[cfg(ink_rts)] Ink(ink::Manager), } + +impl PlatformImpl for PlatformManager { + fn pump(&mut self) -> Result<(), crate::PumpError> { + // deref with `ref mut` bindings prevents err when uninhabited. + match *self { + #[cfg(wl_tablet)] + Self::Wayland(ref mut m) => m.pump(), + #[cfg(xinput2)] + Self::XInput2(ref mut m) => m.pump(), + #[cfg(ink_rts)] + Self::Ink(ref mut m) => m.pump(), + } + } + fn timestamp_granularity(&self) -> Option { + match *self { + #[cfg(wl_tablet)] + Self::Wayland(ref m) => m.timestamp_granularity(), + #[cfg(xinput2)] + Self::XInput2(ref m) => m.timestamp_granularity(), + #[cfg(ink_rts)] + Self::Ink(ref m) => m.timestamp_granularity(), + } + } + fn pads(&self) -> &[crate::pad::Pad] { + match *self { + #[cfg(wl_tablet)] + Self::Wayland(ref m) => m.pads(), + #[cfg(xinput2)] + Self::XInput2(ref m) => m.pads(), + #[cfg(ink_rts)] + Self::Ink(ref m) => m.pads(), + } + } + fn tools(&self) -> &[crate::tool::Tool] { + match *self { + #[cfg(wl_tablet)] + Self::Wayland(ref m) => m.tools(), + #[cfg(xinput2)] + Self::XInput2(ref m) => m.tools(), + #[cfg(ink_rts)] + Self::Ink(ref m) => m.tools(), + } + } + fn tablets(&self) -> &[crate::tablet::Tablet] { + match *self { + #[cfg(wl_tablet)] + Self::Wayland(ref m) => m.tablets(), + #[cfg(xinput2)] + Self::XInput2(ref m) => m.tablets(), + #[cfg(ink_rts)] + Self::Ink(ref m) => m.tablets(), + } + } + fn raw_events(&self) -> RawEventsIter<'_> { + match *self { + #[cfg(wl_tablet)] + Self::Wayland(ref m) => m.raw_events(), + #[cfg(xinput2)] + Self::XInput2(ref m) => m.raw_events(), + #[cfg(ink_rts)] + Self::Ink(ref m) => m.raw_events(), + } + } +} diff --git a/src/platform/xinput2/mod.rs b/src/platform/xinput2/mod.rs index a1d2c72..c2711f9 100644 --- a/src/platform/xinput2/mod.rs +++ b/src/platform/xinput2/mod.rs @@ -7,6 +7,10 @@ use x11rb::{ }, }; +/// If this many milliseconds since last ring interaction, emit an Out event. +const RING_TIMEOUT_MS: Timestamp = 200; +/// If this many milliseconds since last tool interaction, emit an Out event. +const TOOL_TIMEOUT_MS: Timestamp = 500; // Some necessary constants not defined by x11rb: const XI_ALL_DEVICES: u16 = 0; const XI_ALL_MASTER_DEVICES: u8 = 1; @@ -275,11 +279,10 @@ struct AxisInfo { transform: Transform, } -#[derive(Eq, PartialEq)] +#[derive(Eq, PartialEq, Clone, Copy)] enum Phase { In, Down, - Up, Out, } @@ -304,10 +307,107 @@ struct ToolInfo { // A change has occured on this pump that requires a frame event at this time. // (pose, button, enter, ect) frame_pending: Option, + last_interaction: Option, } +impl ToolInfo { + fn take_timeout(&mut self, now: Timestamp) -> bool { + let Some(interaction) = self.last_interaction else { + return false; + }; + + if interaction > now { + return false; + } + + let diff = now - interaction; + + if diff >= TOOL_TIMEOUT_MS { + self.last_interaction = None; + true + } else { + false + } + } + /// Set the current phase of interaction, emitting any needed events to get to that state. + fn set_phase(&mut self, self_id: ID, phase: Phase, events: &mut Vec>) { + enum Transition { + In, + Down, + Out, + Up, + } + // Find the transitions that need to occur, in order. + #[allow(clippy::match_same_arms)] + let changes: &[_] = match (self.phase, phase) { + (Phase::Down, Phase::Out) => &[Transition::Up, Transition::Out], + (Phase::Down, Phase::In) => &[Transition::Up], + (Phase::Down, Phase::Down) => &[], + (Phase::In, Phase::Out) => &[Transition::Out], + (Phase::In, Phase::In) => &[], + (Phase::In, Phase::Down) => &[Transition::Down], + (Phase::Out, Phase::Out) => &[], + (Phase::Out, Phase::In) => &[Transition::In], + (Phase::Out, Phase::Down) => &[Transition::In, Transition::Down], + }; + self.phase = phase; + + for change in changes { + events.push(raw::Event::Tool { + tool: self_id, + event: match change { + Transition::In => raw::ToolEvent::In { + tablet: self.tablet, + }, + Transition::Out => raw::ToolEvent::Out, + Transition::Down => raw::ToolEvent::Down, + Transition::Up => raw::ToolEvent::Up, + }, + }); + } + } + /// If the tool is Out, move it In. no effect if down or in already. + fn ensure_in(&mut self, self_id: ID, events: &mut Vec>) { + if self.phase == Phase::Out { + self.phase = Phase::In; + + events.push(raw::Event::Tool { + tool: self_id, + event: raw::ToolEvent::In { + tablet: self.tablet, + }, + }); + } + } +} + +struct RingInfo { + axis: AxisInfo, + last_interaction: Option, +} +impl RingInfo { + /// Returns true if the ring was interacted but the interaction timed out. + /// When true, emit an Out event. + fn take_timeout(&mut self, now: Timestamp) -> bool { + let Some(interaction) = self.last_interaction else { + return false; + }; + if interaction > now { + return false; + } + + let diff = now - interaction; + + if diff >= RING_TIMEOUT_MS { + self.last_interaction = None; + true + } else { + false + } + } +} struct PadInfo { - ring: Option, + ring: Option, /// The tablet this tool belongs to, based on heuristics, or Dummy. tablet: ID, } @@ -371,7 +471,6 @@ pub struct Manager { events: Vec>, window: x11rb::protocol::xproto::Window, atom_usb_id: Option>, - atom_device_node: Option>, // What is the most recent event timecode? server_time: Timestamp, /// Device ID generation. Increment when one or more devices is removed in a frame. @@ -394,7 +493,7 @@ impl Manager { .unwrap() .reply() .unwrap();*/ - let version = conn.xinput_xi_query_version(2, 2).unwrap().reply().unwrap(); + let version = conn.xinput_xi_query_version(2, 4).unwrap().reply().unwrap(); assert!(version.major_version >= 2); @@ -445,11 +544,6 @@ impl Manager { .ok() .and_then(|resp| resp.reply().ok()) .and_then(|reply| reply.atom.try_into().ok()); - let atom_device_node = conn - .intern_atom(false, b"Device Node") - .ok() - .and_then(|resp| resp.reply().ok()) - .and_then(|reply| reply.atom.try_into().ok()); let mut this = Self { conn, @@ -461,7 +555,6 @@ impl Manager { events: vec![], tablets: vec![], window, - atom_device_node, atom_usb_id, server_time: 0, device_generation: 0, @@ -654,6 +747,7 @@ impl Manager { .unwrap_or_default(), is_grabbed: false, frame_pending: None, + last_interaction: None, }; // Look for axes! @@ -777,7 +871,7 @@ impl Manager { } DeviceType::Pad => { let mut buttons = 0; - let mut ring_info = None; + let mut ring_axis = None; for class in &query.classes { match &class.data { xinput::DeviceClassData::Button(b) => { @@ -807,7 +901,7 @@ impl Manager { } let min = fixed32_to_f32(v.min); let max = fixed32_to_f32(v.max); - ring_info = Some(AxisInfo { + ring_axis = Some(AxisInfo { index: v.number, transform: Transform::BiasScale { bias: -min, @@ -819,13 +913,13 @@ impl Manager { _ => (), } } - if buttons == 0 && ring_info.is_none() { + if buttons == 0 && ring_axis.is_none() { // This pad has no functionality for us. continue; } let mut rings = vec![]; - if ring_info.is_some() { + if ring_axis.is_some() { rings.push(crate::pad::Ring { granularity: None, internal_id: crate::platform::InternalID::XInput2(octotablet_id), @@ -868,7 +962,10 @@ impl Manager { self.pad_infos.insert( octotablet_id, PadInfo { - ring: ring_info, + ring: ring_axis.map(|ring_axis| RingInfo { + axis: ring_axis, + last_interaction: None, + }), tablet: tablet.unwrap_or(ID::EmulatedTablet), }, ); @@ -1143,15 +1240,14 @@ impl Manager { | xinput::XIEventMask::BARRIER_HIT | xinput::XIEventMask::BARRIER_LEAVE // Axis movement - | xinput::XIEventMask::MOTION, + | xinput::XIEventMask::MOTION // Proximity is implicit, i guess. I'm losing my mind. // property change. The only properties we look at are static. // | xinput::XIEventMask::PROPERTY - // Sent when a master device is bound, and the device controlling it - // changes (thus presenting a master with different classes) - // We don't listen for valuators nor buttons on master devices, though! - // | xinput::XIEventMask::DEVICE_CHANGED + // Sent when a different device is controlling a master (dont care) + // or when a physical device changes it's properties (do care) + | xinput::XIEventMask::DEVICE_CHANGED, ] .into(), } @@ -1229,20 +1325,8 @@ impl Manager { } } - // release and out, if need be. - if tool.phase == Phase::Down { - self.events.push(raw::Event::Tool { - tool: id, - event: raw::ToolEvent::Up, - }); - }; - if was_in { - self.events.push(raw::Event::Tool { - tool: id, - event: raw::ToolEvent::Out, - }); - }; - tool.phase = Phase::Out; + tool.set_phase(id, Phase::Out, &mut self.events); + // Don't care if it succeeded or failed. self.conn .xinput_ungrab_device(time, device.id) @@ -1252,37 +1336,105 @@ impl Manager { tool.is_grabbed = false; } } + fn pre_frame_cleanup(&mut self) { + self.events.clear(); + } + fn post_frame_cleanup(&mut self) { + // Emit emulated ring outs. + for (&id, pad) in &mut self.pad_infos { + if let Some(ring) = &mut pad.ring { + if ring.take_timeout(self.server_time) { + self.events.push(raw::Event::Pad { + pad: id, + event: raw::PadEvent::Group { + group: id, + event: raw::PadGroupEvent::Ring { + ring: id, + event: crate::events::TouchStripEvent::Up, + }, + }, + }); + } + } + } + // Emit pending tool frames and emulate Out from timeout. + for (&id, tool) in &mut self.tool_infos { + if let Some(time) = tool.frame_pending.take() { + self.events.push(raw::Event::Tool { + tool: id, + event: raw::ToolEvent::Frame(Some(crate::events::FrameTimestamp( + std::time::Duration::from_millis(time.into()), + ))), + }); + } + if tool.take_timeout(self.server_time) { + tool.set_phase(id, Phase::Out, &mut self.events); + } + } + } } impl super::PlatformImpl for Manager { #[allow(clippy::too_many_lines)] fn pump(&mut self) -> Result<(), crate::PumpError> { - self.events.clear(); + self.pre_frame_cleanup(); let mut has_repopulated = false; while let Ok(Some(event)) = self.conn.poll_for_event() { use x11rb::protocol::Event; match event { + // Devices added, removed, reassigned, etc. + Event::XinputHierarchy(h) => { + self.server_time = h.time; + // for device in h.infos { + // let Ok(device_id) = u8::try_from(device.deviceid) else { + // continue; + //}; + // if let Some((id, info)) = tool_info_mut_from_device_id( + // device_id, + // &mut self.tool_infos, + // self.device_generation, + // ) {} + // if let Some((id, info)) = pad_info_mut_from_device_id( + // device_id, + // &mut self.tool_infos, + // self.device_generation, + // ) {} + // } + // The event does not necessarily reflect *all* changes, the spec specifically says + // that the client should probably just rescan. lol + if !has_repopulated { + has_repopulated = true; + self.repopulate(); + } + } + Event::XinputDeviceChanged(c) => { + // We only care if a physical device's capabilities changed. + if c.reason != xinput::ChangeReason::DEVICE_CHANGE { + continue; + } + } // xwayland fails to emit Leave/Enter when the cursor is warped to/from another window // by a proximity in event. However, it emits a FocusOut/FocusIn for the associated // master keyboard in that case, which we can use to emulate. // On a genuine X11 server this causes the device release logic to happen twice. // Could we just always rely on FocusOut, or would that add more edge cases? Event::XinputLeave(leave) | Event::XinputFocusOut(leave) => { + self.server_time = leave.time; // MASTER POINTER ONLY. Cursor has left the client bounds. self.parent_left(leave.deviceid, leave.time); - self.server_time = leave.time; } Event::XinputEnter(enter) | Event::XinputFocusIn(enter) => { + self.server_time = enter.time; // MASTER POINTER ONLY. Cursor has entered client bounds. self.parent_entered(enter.deviceid, enter.time); - self.server_time = enter.time; } // Proximity (coming in and out of sense range) events. // Not guaranteed to be sent, eg. if the tool comes in proximity while // over a different window. We'll need to emulate the In event in such cases. // Never sent on xwayland. Event::XinputProximityIn(x) => { + self.server_time = x.time; // wh.. why.. let device_id = x.device_id & 0x7f; let Some((id, tool)) = tool_info_mut_from_device_id( @@ -1292,18 +1444,10 @@ impl super::PlatformImpl for Manager { ) else { continue; }; - if tool.phase == Phase::Out { - tool.phase = Phase::In; - self.events.push(raw::Event::Tool { - tool: id, - event: raw::ToolEvent::In { - tablet: tool.tablet, - }, - }); - } - self.server_time = x.time; + tool.ensure_in(id, &mut self.events); } Event::XinputProximityOut(x) => { + self.server_time = x.time; let device_id = x.device_id & 0x7f; let Some((id, tool)) = tool_info_mut_from_device_id( device_id, @@ -1312,26 +1456,14 @@ impl super::PlatformImpl for Manager { ) else { continue; }; - // Emulate Up before out if need be. - if tool.phase == Phase::Down { - self.events.push(raw::Event::Tool { - tool: id, - event: raw::ToolEvent::Up, - }); - } - if tool.phase != Phase::Out { - tool.phase = Phase::Out; - self.events.push(raw::Event::Tool { - tool: id, - event: raw::ToolEvent::Out, - }); - } - self.server_time = x.time; + + tool.set_phase(id, Phase::Out, &mut self.events); } // XinputDeviceButtonPress, ButtonPress, XinputRawButtonPress are red herrings. // Dear X consortium... What the fuck? Event::XinputButtonPress(e) | Event::XinputButtonRelease(e) => { // Tool buttons. + self.server_time = e.time; if e.flags .intersects(xinput::PointerEventFlags::POINTER_EMULATED) { @@ -1350,15 +1482,9 @@ impl super::PlatformImpl for Manager { let button_idx = u16::try_from(e.detail).unwrap(); // Emulate In event if currently out. - if tool.phase == Phase::Out { - self.events.push(raw::Event::Tool { - tool: id, - event: raw::ToolEvent::In { - tablet: tool.tablet, - }, - }); - tool.phase = Phase::Up; - } + tool.ensure_in(id, &mut self.events); + + let pressed = e.event_type == xinput::BUTTON_PRESS_EVENT; // Detail gives the "button index". match button_idx { @@ -1366,25 +1492,11 @@ impl super::PlatformImpl for Manager { 0 => (), // Tip button 1 => { - if e.event_type == xinput::BUTTON_PRESS_EVENT { - if tool.phase == Phase::Down { - continue; - } - tool.phase = Phase::Down; - self.events.push(raw::Event::Tool { - tool: id, - event: raw::ToolEvent::Down, - }); - } else { - if tool.phase == Phase::Up { - continue; - } - tool.phase = Phase::Up; - self.events.push(raw::Event::Tool { - tool: id, - event: raw::ToolEvent::Up, - }); - } + tool.set_phase( + id, + if pressed { Phase::Down } else { Phase::In }, + &mut self.events, + ); } // Other (barrel) button. _ => { @@ -1395,14 +1507,14 @@ impl super::PlatformImpl for Manager { // Already checked != 0 button_idx.try_into().unwrap(), ), - pressed: e.event_type == xinput::BUTTON_PRESS_EVENT, + pressed, }, }); } } - self.server_time = e.time; } Event::XinputMotion(m) => { + self.server_time = m.time; // Tool valuators. let mut try_uwu = || -> Option<()> { let valuator_fetch = |idx: u16| -> Option { @@ -1441,15 +1553,8 @@ impl super::PlatformImpl for Manager { } } - if tool.phase == Phase::Out { - tool.phase = Phase::In; - self.events.push(raw::Event::Tool { - tool: id, - event: raw::ToolEvent::In { - tablet: tool.tablet, - }, - }); - } + tool.ensure_in(id, &mut self.events); + // Access valuators, and map them to our range for the associated axis. let pressure = tool .pressure @@ -1492,7 +1597,6 @@ impl super::PlatformImpl for Manager { if try_uwu().is_none() { //println!("failed to fetch axes."); } - self.server_time = m.time; } Event::XinputDeviceValuator(m) => { // Pad valuators. Instead of the arbtrary number of valuators that the tools @@ -1506,10 +1610,10 @@ impl super::PlatformImpl for Manager { ) else { continue; }; - let Some(ring_info) = pad_info.ring else { + let Some(ring_info) = &mut pad_info.ring else { continue; }; - let absolute_ring_index = ring_info.index; + let absolute_ring_index = ring_info.axis.index; let Some(relative_ring_indox) = absolute_ring_index.checked_sub(u16::from(m.first_valuator)) else { @@ -1524,9 +1628,27 @@ impl super::PlatformImpl for Manager { continue; }; + // If the last interaction timed out, emit an Up. + // This isn't always handled by the end frame logic, since + // the time doesn't update if no x11 events occur. + // A bit of a hole here. since this event doesn't update server time, + // timeouts don't work if no other event type occured in the meantime :V + // THERE's ONLY SO MUCH I CAN DO lol + if ring_info.take_timeout(self.server_time) { + self.events.push(raw::Event::Pad { + pad: id, + event: raw::PadEvent::Group { + group: id, + event: raw::PadGroupEvent::Ring { + ring: id, + event: crate::events::TouchStripEvent::Up, + }, + }, + }); + } + if valuator_value == 0 { - // On release, this is snapped back to zero, but zero is also a valid value. There does not - // seem to be a method of checking when the interaction ended to avoid this. + // On release, this is snapped back to zero, but zero is also a valid value. // Snapping back to zero makes this entirely useless for knob control (which is the primary // purpose of the ring) so we take this little loss. @@ -1541,7 +1663,7 @@ impl super::PlatformImpl for Manager { event: raw::PadGroupEvent::Ring { ring: id, event: crate::events::TouchStripEvent::Pose( - ring_info.transform.transform(valuator_value as f32), + ring_info.axis.transform.transform(valuator_value as f32), ), }, }, @@ -1563,8 +1685,11 @@ impl super::PlatformImpl for Manager { }, }, }); + + ring_info.last_interaction = Some(self.server_time); } Event::XinputDeviceButtonPress(e) | Event::XinputDeviceButtonRelease(e) => { + self.server_time = e.time; // Pad buttons. let Some((id, pad_info)) = pad_mut_from_device_id(e.device_id, &mut self.pads, self.device_generation) @@ -1586,35 +1711,16 @@ impl super::PlatformImpl for Manager { event: raw::PadEvent::Button { // Shift 1-based to 0-based indexing. button_idx: button_idx - 1, + // "Pressed" event code. pressed: e.response_type == 69, }, }); - self.server_time = e.time; - } - Event::XinputHierarchy(h) => { - // The event does not necessarily reflect *all* changes, the spec specifically says - // that the client should probably just rescan. lol - if !has_repopulated { - has_repopulated = true; - self.repopulate(); - } - self.server_time = h.time; } _ => (), } } - // Emit pending frames. - for (id, tool) in &mut self.tool_infos { - if let Some(time) = tool.frame_pending.take() { - self.events.push(raw::Event::Tool { - tool: *id, - event: raw::ToolEvent::Frame(Some(crate::events::FrameTimestamp( - std::time::Duration::from_millis(time.into()), - ))), - }); - } - } + self.post_frame_cleanup(); Ok(()) } From 45c0df846e6c05dd27be6c2366f01531941eceff Mon Sep 17 00:00:00 2001 From: Aspen Pratt Date: Sun, 15 Sep 2024 14:28:55 -0700 Subject: [PATCH 12/15] all xinput2, aside from device enumerate. this horribly broke in/out emulation. --- src/platform/xinput2/mod.rs | 749 +++++++++++++++++------------------- 1 file changed, 355 insertions(+), 394 deletions(-) diff --git a/src/platform/xinput2/mod.rs b/src/platform/xinput2/mod.rs index c2711f9..d416177 100644 --- a/src/platform/xinput2/mod.rs +++ b/src/platform/xinput2/mod.rs @@ -13,14 +13,12 @@ const RING_TIMEOUT_MS: Timestamp = 200; const TOOL_TIMEOUT_MS: Timestamp = 500; // Some necessary constants not defined by x11rb: const XI_ALL_DEVICES: u16 = 0; -const XI_ALL_MASTER_DEVICES: u8 = 1; +const XI_ALL_MASTER_DEVICES: u16 = 1; /// Magic timestamp signalling to the server "now". const NOW_MAGIC: x11rb::protocol::xproto::Timestamp = 0; // Strings are used to communicate the class of device, so we need a hueristic to // find devices we are interested in and a transformation to a more well-documented enum. -// I Could not find a comprehensive guide to the strings used here. -// (I am SURE I saw one at one point, but can't find it again.) So these are just -// the ones I have access to in my testing. +// Strings used here are defined in X11/extensions/XI.h /// X "device_type" atom for [`crate::tablet`]s... const TYPE_TABLET: &str = "TABLET"; /// .. [`crate::pad`]s... @@ -143,9 +141,8 @@ pub(super) enum ID { /// Xinput re-uses the IDs of removed devices. /// Since we need to keep around devices for an extra frame to report added/removed, /// it means a conflict can occur. - generation: u8, - /// XI1 ID. XI2 uses u16 but i'm just generally confused UwU - device_id: std::num::NonZero, + generation: u16, + device_id: std::num::NonZero, }, } @@ -241,6 +238,8 @@ fn pad_maybe_associated_tablet(name: &str) -> Option { fn fixed32_to_f32(fixed: xinput::Fp3232) -> f32 { // Could bit-twiddle these into place instead, likely with more precision. let integral = fixed.integral as f32; + // Funny thing. the spec says that frac is the 'decimal fraction'. + // that's a mighty weird way to spell that -- is this actually in base10? let fractional = fixed.frac as f32 / (u64::from(u32::MAX) + 1) as f32; if fixed.integral.is_positive() { @@ -303,7 +302,7 @@ struct ToolInfo { master_pointer: u16, /// The master keyboard associated with the master pointer. master_keyboard: u16, - is_grabbed: bool, + grabbed: bool, // A change has occured on this pump that requires a frame event at this time. // (pose, button, enter, ect) frame_pending: Option, @@ -410,19 +409,17 @@ struct PadInfo { ring: Option, /// The tablet this tool belongs to, based on heuristics, or Dummy. tablet: ID, -} - -struct OpenDevice { - mask: Vec, - id: u8, + master_pointer: u16, + master_keyboard: u16, + grabbed: bool, } fn tool_info_mut_from_device_id( - id: u8, + id: u16, infos: &mut std::collections::BTreeMap, - now_generation: u8, + now_generation: u16, ) -> Option<(ID, &mut ToolInfo)> { - let non_zero_id = std::num::NonZero::::new(id)?; + let non_zero_id = std::num::NonZero::::new(id)?; let id = ID::ID { generation: now_generation, device_id: non_zero_id, @@ -431,11 +428,11 @@ fn tool_info_mut_from_device_id( infos.get_mut(&id).map(|info| (id, info)) } fn pad_info_mut_from_device_id( - id: u8, + id: u16, infos: &mut std::collections::BTreeMap, - now_generation: u8, + now_generation: u16, ) -> Option<(ID, &mut PadInfo)> { - let non_zero_id = std::num::NonZero::::new(id)?; + let non_zero_id = std::num::NonZero::::new(id)?; let id = ID::ID { generation: now_generation, device_id: non_zero_id, @@ -444,11 +441,11 @@ fn pad_info_mut_from_device_id( infos.get_mut(&id).map(|info| (id, info)) } fn pad_mut_from_device_id( - id: u8, + id: u16, infos: &mut [crate::pad::Pad], - now_generation: u8, + now_generation: u16, ) -> Option<(ID, &mut crate::pad::Pad)> { - let non_zero_id = std::num::NonZero::::new(id)?; + let non_zero_id = std::num::NonZero::::new(id)?; let id = ID::ID { generation: now_generation, device_id: non_zero_id, @@ -463,7 +460,6 @@ fn pad_mut_from_device_id( pub struct Manager { conn: x11rb::rust_connection::RustConnection, tool_infos: std::collections::BTreeMap, - open_devices: Vec, tools: Vec, pad_infos: std::collections::BTreeMap, pads: Vec, @@ -474,7 +470,7 @@ pub struct Manager { // What is the most recent event timecode? server_time: Timestamp, /// Device ID generation. Increment when one or more devices is removed in a frame. - device_generation: u8, + device_generation: u16, } impl Manager { @@ -494,6 +490,10 @@ impl Manager { .reply() .unwrap();*/ let version = conn.xinput_xi_query_version(2, 4).unwrap().reply().unwrap(); + println!( + "Server supports v{}.{}", + version.major_version, version.minor_version + ); assert!(version.major_version >= 2); @@ -549,7 +549,6 @@ impl Manager { conn, tool_infos: std::collections::BTreeMap::new(), pad_infos: std::collections::BTreeMap::new(), - open_devices: vec![], tools: vec![], pads: vec![], events: vec![], @@ -576,17 +575,10 @@ impl Manager { self.tool_infos.clear(); self.pad_infos.clear(); - if !self.open_devices.is_empty() { - self.device_generation = self.device_generation.wrapping_add(1); - } - - for device in self.open_devices.drain(..) { - // Don't care if the effects of closure went through, just - // that it's sent. Some may fail. Fixme! - let _ = self.conn.xinput_close_device(device.id).unwrap(); - } // Tools ids to bulk-enable events on. - let mut tool_listen_events = vec![XI_ALL_MASTER_DEVICES]; + let mut tool_listen_events = vec![]; + // Pad ids to bulk-enable events on. + let mut pad_listen_events = vec![]; // Okay, this is weird. There are two very similar functions, xi_query_device and list_input_devices. // The venne diagram of the data contained within their responses is nearly a circle, however each @@ -618,7 +610,7 @@ impl Manager { .zip(device_list.devices.into_iter()) { // Zero is a special value (ALL_DEVICES), and can't be used by a device. - let nonzero_id = std::num::NonZero::::new(device.device_id).unwrap(); + let nonzero_id = std::num::NonZero::new(u16::from(device.device_id)).unwrap(); let octotablet_id = ID::ID { generation: self.device_generation, device_id: nonzero_id, @@ -704,11 +696,10 @@ impl Manager { .iter() .find(|info| info.name == expected.as_bytes())?; - let id = u8::try_from(tablet_info.deviceid).ok()?; Some(ID::ID { generation: self.device_generation, // 0 is a special value, this is infallible. - device_id: id.try_into().unwrap(), + device_id: tablet_info.deviceid.try_into().unwrap(), }) }); @@ -745,7 +736,7 @@ impl Manager { }) // Above search should be infallible but I trust nothing at this point. .unwrap_or_default(), - is_grabbed: false, + grabbed: false, frame_pending: None, last_interaction: None, }; @@ -865,11 +856,15 @@ impl Manager { } } - tool_listen_events.push(device.device_id); + tool_listen_events.push(u16::from(device.device_id)); self.tools.push(octotablet_info); self.tool_infos.insert(octotablet_id, x11_info); } DeviceType::Pad => { + if query.type_ == xinput::DeviceType::FLOATING_SLAVE { + // We need master attachments in order to grab. + continue; + } let mut buttons = 0; let mut ring_axis = None; for class in &query.classes { @@ -941,6 +936,8 @@ impl Manager { groups: vec![group], }); + pad_listen_events.push(u16::from(device.device_id)); + // Find the tablet this belongs to. let tablet = raw_name .as_deref() @@ -951,13 +948,39 @@ impl Manager { .infos .iter() .find(|info| info.name == expected.as_bytes())?; - let id = u8::try_from(tablet_info.deviceid).ok()?; + Some(ID::ID { generation: self.device_generation, // 0 is ALL_DEVICES, this is infallible. - device_id: id.try_into().unwrap(), + device_id: tablet_info.deviceid.try_into().unwrap(), }) }); + let primary_master = query.attachment; + let other_master = device_queries + .infos + .iter() + .find_map(|q| { + // Find the info for the master pointer + if q.deviceid == primary_master { + // Look at the master pointer's attachment, + // which is the associated master keyboard's ID. + Some(q.attachment) + } else { + None + } + }) + // Above search should be infallible but I trust nothing at this point. + .unwrap_or_default(); + + // Depending on what flavor of device we are, the master is pointer or keyboard. + let (master_pointer, master_keyboard) = if matches!( + query.type_, + xinput::DeviceType::MASTER_KEYBOARD | xinput::DeviceType::SLAVE_KEYBOARD + ) { + (primary_master, other_master) + } else { + (other_master, primary_master) + }; self.pad_infos.insert( octotablet_id, @@ -966,7 +989,10 @@ impl Manager { axis: ring_axis, last_interaction: None, }), + master_pointer, + master_keyboard, tablet: tablet.unwrap_or(ID::EmulatedTablet), + grabbed: false, }, ); } @@ -1018,129 +1044,6 @@ impl Manager { self.tablets.push(tablet); } } - - // If we got to this point, we accepted the device. - // Request the server give us access to this device's events. - // Not sure what this reply data is for. - let repl = self - .conn - .xinput_open_device(device.device_id) - .unwrap() - .reply() - .unwrap(); - - // Enable event aspects. Why is this a different process than select events? - // Scientists are working day and night to find the answer. - let mut enable = Vec::<(u8, u8)>::new(); - for class in repl.class_info { - const DEVICE_KEY_PRESS: u8 = 0; - const DEVICE_KEY_RELEASE: u8 = 1; - const DEVICE_BUTTON_PRESS: u8 = 0; - const DEVICE_BUTTON_RELEASE: u8 = 1; - const DEVICE_MOTION_NOTIFY: u8 = 0; - const DEVICE_FOCUS_IN: u8 = 0; - const DEVICE_FOCUS_OUT: u8 = 1; - const PROXIMITY_IN: u8 = 0; - const PROXIMITY_OUT: u8 = 1; - const DEVICE_STATE_NOTIFY: u8 = 0; - const DEVICE_MAPPING_NOTIFY: u8 = 1; - const CHANGE_DEVICE_NOTIFY: u8 = 2; - // Reverse engineered from Xinput.h, and xinput/test.c - // #define FindTypeAndClass(device,proximity_in_type,desired_event_mask,ProximityClass,offset) \ - // FindTypeAndClass(device, proximity_in_type, desired_event_mask, ProximityClass, _proximityIn) - // == EXPANDED: == - // { - // int _i; - // XInputClassInfo *_ip; - // proximity_in_type = 0; - // desired_event_mask = 0; - // _i = 0; - // _ip = ((XDevice *) device)->classes; - // for (;_i< ((XDevice *) device)->num_classes; _i++, _ip++) { - // if (_ip->input_class == ProximityClass) { - // proximity_in_type = _ip->event_type_base + 0; - // desired_event_mask = ((XDevice *) device)->device_id << 8 | proximity_in_type; - // } - // } - // } - - // (base, offset) - - match class.class_id { - // Constants taken from XInput.h - xinput::InputClass::PROXIMITY => { - enable.extend_from_slice(&[ - (class.event_type_base, PROXIMITY_IN), - (class.event_type_base, PROXIMITY_OUT), - ]); - } - xinput::InputClass::BUTTON => { - enable.extend_from_slice(&[ - (class.event_type_base, DEVICE_BUTTON_PRESS), - (class.event_type_base, DEVICE_BUTTON_RELEASE), - ]); - } - xinput::InputClass::FOCUS => { - enable.extend_from_slice(&[ - (class.event_type_base, DEVICE_FOCUS_IN), - (class.event_type_base, DEVICE_FOCUS_OUT), - ]); - } - xinput::InputClass::OTHER => { - enable.extend_from_slice(&[ - (class.event_type_base, DEVICE_STATE_NOTIFY), - (class.event_type_base, DEVICE_MAPPING_NOTIFY), - (class.event_type_base, CHANGE_DEVICE_NOTIFY), - // PROPERTY_NOTIFY - ]); - } - xinput::InputClass::VALUATOR => { - enable.push((class.event_type_base, DEVICE_MOTION_NOTIFY)); - } - xinput::InputClass::KEY => { - enable.extend_from_slice(&[ - (class.event_type_base, DEVICE_KEY_PRESS), - (class.event_type_base, DEVICE_KEY_RELEASE), - ]); - } - _ => (), - } - } - let masks = enable - .into_iter() - .map(|(base, offset)| -> u32 { - u32::from(device.device_id) << 8 | (u32::from(base) + u32::from(offset)) - }) - .collect::>(); - - // Keep track so we can close it later! - self.open_devices.push(OpenDevice { - mask: masks.clone(), - id: device.device_id, - }); - - self.conn - .xinput_select_extension_event(self.window, &masks) - .unwrap() - .check() - .unwrap(); - /*let status = self - .conn - .xinput_grab_device( - self.window, - NOW_MAGIC, - x11rb::protocol::xproto::GrabMode::SYNC, - x11rb::protocol::xproto::GrabMode::SYNC, - false, - device.device_id, - &masks, - ) - .unwrap() - .reply() - .unwrap() - .status; - - println!("Grab {} - {:?}", device.device_id, status);*/ } // True if any tablet refers to a non-existant device. @@ -1158,7 +1061,7 @@ impl Manager { .iter() .any(|tablet| match *tablet.internal_id.unwrap_xinput2() { ID::ID { device_id, .. } => device_id == desired_tablet, - _ => false, + ID::EmulatedTablet => false, }) { tool.tablet = ID::EmulatedTablet; @@ -1182,7 +1085,7 @@ impl Manager { .iter() .any(|tablet| match *tablet.internal_id.unwrap_xinput2() { ID::ID { device_id, .. } => device_id == desired_tablet, - _ => false, + ID::EmulatedTablet => false, }) { wants_dummy_tablet = true; @@ -1220,12 +1123,12 @@ impl Manager { return; } - // Register with the server that we want to listen in on these events for all current devices: - let interest = tool_listen_events + // Tool events: + let mut interest = tool_listen_events .into_iter() - .map(|id| { + .map(|deviceid| { xinput::EventMask { - deviceid: id.into(), + deviceid, mask: [ // Barrel and tip buttons xinput::XIEventMask::BUTTON_PRESS @@ -1236,10 +1139,12 @@ impl Manager { // Also enter and leave? Doesn't work. | xinput::XIEventMask::FOCUS_IN | xinput::XIEventMask::FOCUS_OUT - // No idea, undocumented and doesn't work. - | xinput::XIEventMask::BARRIER_HIT - | xinput::XIEventMask::BARRIER_LEAVE + // Touches user-defined pointer barrier + // | xinput::XIEventMask::BARRIER_HIT + // | xinput::XIEventMask::BARRIER_LEAVE // Axis movement + // since XI2.4, RAW_MOTION should work here, and give us events regardless + // of grab state. it does not work. COol i love this API | xinput::XIEventMask::MOTION // Proximity is implicit, i guess. I'm losing my mind. @@ -1254,6 +1159,40 @@ impl Manager { }) .collect::>(); + // Pad events: + interest.extend(pad_listen_events.into_iter().map(|deviceid| { + xinput::EventMask { + deviceid, + mask: [ + // Barrel and tip buttons + xinput::XIEventMask::BUTTON_PRESS + | xinput::XIEventMask::BUTTON_RELEASE + // Axis movement, for pads this is the Ring (plural?) + | xinput::XIEventMask::MOTION + // property change. The only properties we look at are static. + // | xinput::XIEventMask::PROPERTY + // Sent when a different device is controlling a master (dont care) + // or when a physical device changes it's properties (do care) + | xinput::XIEventMask::DEVICE_CHANGED, + ] + .into(), + } + })); + + // Pointer events: + interest.push(xinput::EventMask { + deviceid: XI_ALL_MASTER_DEVICES, + mask: [ + // Pointer coming into and out of our client + xinput::XIEventMask::ENTER + | xinput::XIEventMask::LEAVE + // Keyboard focus coming into and out of our client. + | xinput::XIEventMask::FOCUS_IN + | xinput::XIEventMask::FOCUS_OUT, + ] + .into(), + }); + self.conn .xinput_xi_select_events(self.window, &interest) .unwrap() @@ -1261,51 +1200,86 @@ impl Manager { .unwrap(); } fn parent_entered(&mut self, master: u16, time: Timestamp) { - for device in &self.open_devices { - let Some((_, tool)) = tool_info_mut_from_device_id( - device.id, - &mut self.tool_infos, - self.device_generation, - ) else { - continue; - }; + // Grab tools. + for (&id, tool) in &mut self.tool_infos { let is_child = tool.master_pointer == master || tool.master_keyboard == master; - if tool.is_grabbed || !is_child { + if tool.grabbed || !is_child { continue; } + println!("Grabbed Uwu"); // Don't care if it succeeded or failed. - let _ = self + let status = self + .conn + .xinput_xi_grab_device( + self.window, + time, + // don't override visual cursor. + 0, + match id { + ID::ID { device_id, .. } => device_id.get(), + ID::EmulatedTablet => unreachable!(), + }, + // Allow the device to continue sending events + x11rb::protocol::xproto::GrabMode::ASYNC, + // Allow other devices to continue sending events. + // There's some reason to use SYNC here, meaning the master won't update, + // as then Winit won't send pointer events in addition to octotablet's events. + x11rb::protocol::xproto::GrabMode::ASYNC, + // Grab the events we already asked for. + xinput::GrabOwner::OWNER, + &[], + ) + .unwrap() + .reply() + .unwrap() + .status; + if status == x11rb::protocol::xproto::GrabStatus::SUCCESS { + tool.grabbed = true; + } + } + // Grab pads. + for (id, pad) in &mut self.pad_infos { + let is_child = pad.master_pointer == master || pad.master_keyboard == master; + if pad.grabbed || !is_child { + continue; + } + let status = self .conn - .xinput_grab_device( + .xinput_xi_grab_device( self.window, time, + // don't override visual cursor. + 0, + match id { + ID::ID { device_id, .. } => device_id.get(), + ID::EmulatedTablet => unreachable!(), + }, // Allow the device to continue sending events x11rb::protocol::xproto::GrabMode::ASYNC, // Allow other devices to continue sending events. + // There's some reason to use SYNC here, meaning the master won't update, + // as then Winit won't send pointer events in addition to octotablet's events. x11rb::protocol::xproto::GrabMode::ASYNC, - // Doesn't work as documented, I have no idea. - true, - device.id, - &device.mask, + // Grab the events we already asked for. + xinput::GrabOwner::OWNER, + &[], ) .unwrap() .reply() - .unwrap(); - tool.is_grabbed = true; + .unwrap() + .status; + + if status == x11rb::protocol::xproto::GrabStatus::SUCCESS { + pad.grabbed = true; + } } } fn parent_left(&mut self, master: u16, time: Timestamp) { - for device in &self.open_devices { - let Some((id, tool)) = tool_info_mut_from_device_id( - device.id, - &mut self.tool_infos, - self.device_generation, - ) else { - continue; - }; + // Release tools. + for (&id, tool) in &mut self.tool_infos { let is_child = tool.master_pointer == master || tool.master_keyboard == master; - if !tool.is_grabbed || !is_child { + if !tool.grabbed || !is_child { continue; } @@ -1325,15 +1299,48 @@ impl Manager { } } + println!("Released uwU"); tool.set_phase(id, Phase::Out, &mut self.events); + let succeeded = self + .conn + .xinput_xi_ungrab_device( + time, + match id { + ID::ID { device_id, .. } => device_id.get(), + ID::EmulatedTablet => unreachable!(), + }, + ) + .unwrap() + .check() + .is_ok(); + if succeeded { + tool.grabbed = false; + } + } + // Release pads. + for (&id, pad) in &mut self.pad_infos { + let is_child = pad.master_pointer == master || pad.master_keyboard == master; + if !pad.grabbed || !is_child { + continue; + } + // Don't care if it succeeded or failed. - self.conn - .xinput_ungrab_device(time, device.id) + let succeeded = self + .conn + .xinput_xi_ungrab_device( + time, + match id { + ID::ID { device_id, .. } => device_id.get(), + ID::EmulatedTablet => unreachable!(), + }, + ) .unwrap() .check() - .unwrap(); - tool.is_grabbed = false; + .is_ok(); + if succeeded { + pad.grabbed = false; + } } } fn pre_frame_cleanup(&mut self) { @@ -1429,38 +1436,6 @@ impl super::PlatformImpl for Manager { // MASTER POINTER ONLY. Cursor has entered client bounds. self.parent_entered(enter.deviceid, enter.time); } - // Proximity (coming in and out of sense range) events. - // Not guaranteed to be sent, eg. if the tool comes in proximity while - // over a different window. We'll need to emulate the In event in such cases. - // Never sent on xwayland. - Event::XinputProximityIn(x) => { - self.server_time = x.time; - // wh.. why.. - let device_id = x.device_id & 0x7f; - let Some((id, tool)) = tool_info_mut_from_device_id( - device_id, - &mut self.tool_infos, - self.device_generation, - ) else { - continue; - }; - tool.ensure_in(id, &mut self.events); - } - Event::XinputProximityOut(x) => { - self.server_time = x.time; - let device_id = x.device_id & 0x7f; - let Some((id, tool)) = tool_info_mut_from_device_id( - device_id, - &mut self.tool_infos, - self.device_generation, - ) else { - continue; - }; - - tool.set_phase(id, Phase::Out, &mut self.events); - } - // XinputDeviceButtonPress, ButtonPress, XinputRawButtonPress are red herrings. - // Dear X consortium... What the fuck? Event::XinputButtonPress(e) | Event::XinputButtonRelease(e) => { // Tool buttons. self.server_time = e.time; @@ -1470,71 +1445,99 @@ impl super::PlatformImpl for Manager { // Key press emulation from scroll wheel. continue; } - let device_id = u8::try_from(e.deviceid).unwrap(); - let Some((id, tool)) = tool_info_mut_from_device_id( - device_id, - &mut self.tool_infos, - self.device_generation, - ) else { - continue; - }; - - let button_idx = u16::try_from(e.detail).unwrap(); - - // Emulate In event if currently out. - tool.ensure_in(id, &mut self.events); let pressed = e.event_type == xinput::BUTTON_PRESS_EVENT; - // Detail gives the "button index". - match button_idx { - // Doesn't occur, I don't think. - 0 => (), - // Tip button - 1 => { - tool.set_phase( - id, - if pressed { Phase::Down } else { Phase::In }, - &mut self.events, - ); + if let Some((id, tool)) = tool_info_mut_from_device_id( + e.deviceid, + &mut self.tool_infos, + self.device_generation, + ) { + let Ok(button_idx) = u16::try_from(e.detail) else { + continue; + }; + // Emulate In event if currently out. + tool.ensure_in(id, &mut self.events); + + // Detail gives the "button index". + match button_idx { + // Doesn't occur, I don't think. + 0 => (), + // Tip button + 1 => { + tool.set_phase( + id, + if pressed { Phase::Down } else { Phase::In }, + &mut self.events, + ); + } + // Other (barrel) button. + _ => { + self.events.push(raw::Event::Tool { + tool: id, + event: raw::ToolEvent::Button { + button_id: crate::platform::ButtonID::XInput2( + // Already checked != 0 + button_idx.try_into().unwrap(), + ), + pressed, + }, + }); + } } - // Other (barrel) button. - _ => { - self.events.push(raw::Event::Tool { - tool: id, - event: raw::ToolEvent::Button { - button_id: crate::platform::ButtonID::XInput2( - // Already checked != 0 - button_idx.try_into().unwrap(), - ), - pressed, - }, - }); + } else if let Some((id, pad)) = + pad_mut_from_device_id(e.deviceid, &mut self.pads, self.device_generation) + { + let button_idx = e.detail; + if button_idx == 0 || button_idx > pad.total_buttons { + // Okay, there's a weird off-by-one here, that even throws off the `xinput` debug + // utility. My Intuos Pro S reports 11 buttons, but the maximum button index is.... 11, + // which is clearly invalid. Silly. + // I interpret this as it actually being [1, max_button] instead of [0, max_button) + continue; } - } + + self.events.push(raw::Event::Pad { + pad: id, + event: raw::PadEvent::Button { + // Shift 1-based to 0-based indexing. + button_idx: button_idx - 1, + // "Pressed" event code. + pressed, + }, + }); + }; } Event::XinputMotion(m) => { + // Tool valuators and pad rings. self.server_time = m.time; - // Tool valuators. - let mut try_uwu = || -> Option<()> { - let valuator_fetch = |idx: u16| -> Option { - // Check that it's not masked out- - let word_idx = idx / u32::BITS as u16; - let bit_idx = idx % u32::BITS as u16; - let word = m.valuator_mask.get(usize::from(word_idx))?; - - // This valuator did not report, value is undefined. - if word & (1 << bit_idx as u32) == 0 { - return None; - } - // Fetch it! - m.axisvalues.get(usize::from(idx)).copied() - }; + let valuator_fetch = |idx: u16| -> Option { + // Check that it's not masked out- + let word_idx = idx / u32::BITS as u16; + let bit_idx = idx % u32::BITS as u16; + let word = m.valuator_mask.get(usize::from(word_idx))?; + + // This valuator did not report, value is undefined. + if word & (1 << u32::from(bit_idx)) == 0 { + return None; + } - let device_id = m.deviceid.try_into().ok()?; + // Quirk (why can't we have nice things) + // Pad rings report a mask that the valuator is in the 5th position, + // but then only report a single valuator at idx 0, which contains the value. + // The spec states that this is supposed to be a non-sparse array. oh well. + if m.axisvalues.len() == 1 { + return m.axisvalues.first().copied(); + } + + // Fetch it! + m.axisvalues.get(usize::from(idx)).copied() + }; + + let mut try_tool = || -> Option<()> { let (id, tool) = tool_info_mut_from_device_id( - device_id, + m.deviceid, &mut self.tool_infos, self.device_generation, )?; @@ -1594,128 +1597,86 @@ impl super::PlatformImpl for Manager { }); Some(()) }; - if try_uwu().is_none() { - //println!("failed to fetch axes."); - } - } - Event::XinputDeviceValuator(m) => { - // Pad valuators. Instead of the arbtrary number of valuators that the tools - // are sent, this sends in groups of six. Ignore all of them except the packet that - // contains our ring value. - - let Some((id, pad_info)) = pad_info_mut_from_device_id( - m.device_id, - &mut self.pad_infos, - self.device_generation, - ) else { - continue; - }; - let Some(ring_info) = &mut pad_info.ring else { - continue; - }; - let absolute_ring_index = ring_info.axis.index; - let Some(relative_ring_indox) = - absolute_ring_index.checked_sub(u16::from(m.first_valuator)) - else { - continue; - }; - if relative_ring_indox >= m.num_valuators.into() { + if try_tool().is_some() { continue; } + let mut try_pad = || -> Option<()> { + let (id, pad) = pad_info_mut_from_device_id( + m.deviceid, + &mut self.pad_infos, + self.device_generation, + )?; + let ring_info = pad.ring.as_mut()?; + println!("Looking for {}", ring_info.axis.index); + let raw_valuator_value = valuator_fetch(ring_info.axis.index)?; + let transformed_valuator_value = + ring_info.axis.transform.transform_fixed(raw_valuator_value); + + if ring_info.take_timeout(self.server_time) { + self.events.push(raw::Event::Pad { + pad: id, + event: raw::PadEvent::Group { + group: id, + event: raw::PadGroupEvent::Ring { + ring: id, + event: crate::events::TouchStripEvent::Up, + }, + }, + }); + } - let Some(&valuator_value) = m.valuators.get(usize::from(relative_ring_indox)) - else { - continue; - }; + if raw_valuator_value + == (xinput::Fp3232 { + integral: 0, + frac: 0, + }) + { + // On release, this is snapped back to zero, but zero is also a valid value. + + // Snapping back to zero makes this entirely useless for knob control (which is the primary + // purpose of the ring) so we take this little loss. + return None; + } - // If the last interaction timed out, emit an Up. - // This isn't always handled by the end frame logic, since - // the time doesn't update if no x11 events occur. - // A bit of a hole here. since this event doesn't update server time, - // timeouts don't work if no other event type occured in the meantime :V - // THERE's ONLY SO MUCH I CAN DO lol - if ring_info.take_timeout(self.server_time) { self.events.push(raw::Event::Pad { pad: id, event: raw::PadEvent::Group { group: id, event: raw::PadGroupEvent::Ring { ring: id, - event: crate::events::TouchStripEvent::Up, + event: crate::events::TouchStripEvent::Pose( + transformed_valuator_value, + ), }, }, }); - } - if valuator_value == 0 { - // On release, this is snapped back to zero, but zero is also a valid value. - - // Snapping back to zero makes this entirely useless for knob control (which is the primary - // purpose of the ring) so we take this little loss. - continue; - } - - // About to emit events. Emit frame if the time differs. - self.events.push(raw::Event::Pad { - pad: id, - event: raw::PadEvent::Group { - group: id, - event: raw::PadGroupEvent::Ring { - ring: id, - event: crate::events::TouchStripEvent::Pose( - ring_info.axis.transform.transform(valuator_value as f32), - ), - }, - }, - }); - // Weirdly, this event is the only one without a timestamp. - // So, we track the current time in all the other events, and can - // guestimate based on that. - self.events.push(raw::Event::Pad { - pad: id, - event: raw::PadEvent::Group { - group: id, - event: raw::PadGroupEvent::Ring { - ring: id, - event: crate::events::TouchStripEvent::Frame(Some( - crate::events::FrameTimestamp( - std::time::Duration::from_millis(self.server_time.into()), - ), - )), + self.events.push(raw::Event::Pad { + pad: id, + event: raw::PadEvent::Group { + group: id, + event: raw::PadGroupEvent::Ring { + ring: id, + event: crate::events::TouchStripEvent::Frame(Some( + crate::events::FrameTimestamp( + std::time::Duration::from_millis( + self.server_time.into(), + ), + ), + )), + }, }, - }, - }); - - ring_info.last_interaction = Some(self.server_time); - } - Event::XinputDeviceButtonPress(e) | Event::XinputDeviceButtonRelease(e) => { - self.server_time = e.time; - // Pad buttons. - let Some((id, pad_info)) = - pad_mut_from_device_id(e.device_id, &mut self.pads, self.device_generation) - else { - continue; - }; + }); - let button_idx = u32::from(e.detail); - if button_idx == 0 || button_idx > pad_info.total_buttons { - // Okay, there's a weird off-by-one here, that even throws off the `xinput` debug - // utility. My Intuos Pro S reports 11 buttons, but the maximum button index is.... 11, - // which is clearly invalid. Silly. - // I interpret this as it actually being [1, max_button] instead of [0, max_button) - continue; - } + ring_info.last_interaction = Some(self.server_time); - self.events.push(raw::Event::Pad { - pad: id, - event: raw::PadEvent::Button { - // Shift 1-based to 0-based indexing. - button_idx: button_idx - 1, - // "Pressed" event code. - pressed: e.response_type == 69, - }, - }); + Some(()) + }; + let _ = try_pad(); } + // DeviceValuator, DeviceButton{Pressed, Released}, Proximity{In, Out} are red herrings + // left over from XI 1.x and are never recieved. Don't fall for it! + // It is strange, but XI 2 has no concept of proximity, even though XI 1 does. _ => (), } } From 3c8fee75af005fadd418a0a2b99c1350383aab3e Mon Sep 17 00:00:00 2001 From: Aspen Pratt Date: Thu, 19 Sep 2024 08:29:54 -0700 Subject: [PATCH 13/15] remove legacy grab --- src/platform/xinput2/mod.rs | 166 ++---------------------------------- 1 file changed, 5 insertions(+), 161 deletions(-) diff --git a/src/platform/xinput2/mod.rs b/src/platform/xinput2/mod.rs index d416177..631a1eb 100644 --- a/src/platform/xinput2/mod.rs +++ b/src/platform/xinput2/mod.rs @@ -302,7 +302,6 @@ struct ToolInfo { master_pointer: u16, /// The master keyboard associated with the master pointer. master_keyboard: u16, - grabbed: bool, // A change has occured on this pump that requires a frame event at this time. // (pose, button, enter, ect) frame_pending: Option, @@ -409,9 +408,6 @@ struct PadInfo { ring: Option, /// The tablet this tool belongs to, based on heuristics, or Dummy. tablet: ID, - master_pointer: u16, - master_keyboard: u16, - grabbed: bool, } fn tool_info_mut_from_device_id( @@ -736,7 +732,6 @@ impl Manager { }) // Above search should be infallible but I trust nothing at this point. .unwrap_or_default(), - grabbed: false, frame_pending: None, last_interaction: None, }; @@ -955,32 +950,6 @@ impl Manager { device_id: tablet_info.deviceid.try_into().unwrap(), }) }); - let primary_master = query.attachment; - let other_master = device_queries - .infos - .iter() - .find_map(|q| { - // Find the info for the master pointer - if q.deviceid == primary_master { - // Look at the master pointer's attachment, - // which is the associated master keyboard's ID. - Some(q.attachment) - } else { - None - } - }) - // Above search should be infallible but I trust nothing at this point. - .unwrap_or_default(); - - // Depending on what flavor of device we are, the master is pointer or keyboard. - let (master_pointer, master_keyboard) = if matches!( - query.type_, - xinput::DeviceType::MASTER_KEYBOARD | xinput::DeviceType::SLAVE_KEYBOARD - ) { - (primary_master, other_master) - } else { - (other_master, primary_master) - }; self.pad_infos.insert( octotablet_id, @@ -989,10 +958,7 @@ impl Manager { axis: ring_axis, last_interaction: None, }), - master_pointer, - master_keyboard, tablet: tablet.unwrap_or(ID::EmulatedTablet), - grabbed: false, }, ); } @@ -1133,12 +1099,10 @@ impl Manager { // Barrel and tip buttons xinput::XIEventMask::BUTTON_PRESS | xinput::XIEventMask::BUTTON_RELEASE - // Cursor entering and leaving client area. Doesn't work. - | xinput::XIEventMask::ENTER - | xinput::XIEventMask::LEAVE - // Also enter and leave? Doesn't work. - | xinput::XIEventMask::FOCUS_IN - | xinput::XIEventMask::FOCUS_OUT + // Cursor entering and leaving client area. Doesn't work, + // perhaps it's for master pointers only. + // | xinput::XIEventMask::ENTER + // | xinput::XIEventMask::LEAVE // Touches user-defined pointer barrier // | xinput::XIEventMask::BARRIER_HIT // | xinput::XIEventMask::BARRIER_LEAVE @@ -1146,7 +1110,6 @@ impl Manager { // since XI2.4, RAW_MOTION should work here, and give us events regardless // of grab state. it does not work. COol i love this API | xinput::XIEventMask::MOTION - // Proximity is implicit, i guess. I'm losing my mind. // property change. The only properties we look at are static. // | xinput::XIEventMask::PROPERTY @@ -1199,87 +1162,11 @@ impl Manager { .check() .unwrap(); } - fn parent_entered(&mut self, master: u16, time: Timestamp) { - // Grab tools. - for (&id, tool) in &mut self.tool_infos { - let is_child = tool.master_pointer == master || tool.master_keyboard == master; - if tool.grabbed || !is_child { - continue; - } - - println!("Grabbed Uwu"); - // Don't care if it succeeded or failed. - let status = self - .conn - .xinput_xi_grab_device( - self.window, - time, - // don't override visual cursor. - 0, - match id { - ID::ID { device_id, .. } => device_id.get(), - ID::EmulatedTablet => unreachable!(), - }, - // Allow the device to continue sending events - x11rb::protocol::xproto::GrabMode::ASYNC, - // Allow other devices to continue sending events. - // There's some reason to use SYNC here, meaning the master won't update, - // as then Winit won't send pointer events in addition to octotablet's events. - x11rb::protocol::xproto::GrabMode::ASYNC, - // Grab the events we already asked for. - xinput::GrabOwner::OWNER, - &[], - ) - .unwrap() - .reply() - .unwrap() - .status; - if status == x11rb::protocol::xproto::GrabStatus::SUCCESS { - tool.grabbed = true; - } - } - // Grab pads. - for (id, pad) in &mut self.pad_infos { - let is_child = pad.master_pointer == master || pad.master_keyboard == master; - if pad.grabbed || !is_child { - continue; - } - let status = self - .conn - .xinput_xi_grab_device( - self.window, - time, - // don't override visual cursor. - 0, - match id { - ID::ID { device_id, .. } => device_id.get(), - ID::EmulatedTablet => unreachable!(), - }, - // Allow the device to continue sending events - x11rb::protocol::xproto::GrabMode::ASYNC, - // Allow other devices to continue sending events. - // There's some reason to use SYNC here, meaning the master won't update, - // as then Winit won't send pointer events in addition to octotablet's events. - x11rb::protocol::xproto::GrabMode::ASYNC, - // Grab the events we already asked for. - xinput::GrabOwner::OWNER, - &[], - ) - .unwrap() - .reply() - .unwrap() - .status; - - if status == x11rb::protocol::xproto::GrabStatus::SUCCESS { - pad.grabbed = true; - } - } - } fn parent_left(&mut self, master: u16, time: Timestamp) { // Release tools. for (&id, tool) in &mut self.tool_infos { let is_child = tool.master_pointer == master || tool.master_keyboard == master; - if !tool.grabbed || !is_child { + if !is_child { continue; } @@ -1299,48 +1186,7 @@ impl Manager { } } - println!("Released uwU"); tool.set_phase(id, Phase::Out, &mut self.events); - - let succeeded = self - .conn - .xinput_xi_ungrab_device( - time, - match id { - ID::ID { device_id, .. } => device_id.get(), - ID::EmulatedTablet => unreachable!(), - }, - ) - .unwrap() - .check() - .is_ok(); - if succeeded { - tool.grabbed = false; - } - } - // Release pads. - for (&id, pad) in &mut self.pad_infos { - let is_child = pad.master_pointer == master || pad.master_keyboard == master; - if !pad.grabbed || !is_child { - continue; - } - - // Don't care if it succeeded or failed. - let succeeded = self - .conn - .xinput_xi_ungrab_device( - time, - match id { - ID::ID { device_id, .. } => device_id.get(), - ID::EmulatedTablet => unreachable!(), - }, - ) - .unwrap() - .check() - .is_ok(); - if succeeded { - pad.grabbed = false; - } } } fn pre_frame_cleanup(&mut self) { @@ -1434,7 +1280,6 @@ impl super::PlatformImpl for Manager { Event::XinputEnter(enter) | Event::XinputFocusIn(enter) => { self.server_time = enter.time; // MASTER POINTER ONLY. Cursor has entered client bounds. - self.parent_entered(enter.deviceid, enter.time); } Event::XinputButtonPress(e) | Event::XinputButtonRelease(e) => { // Tool buttons. @@ -1607,7 +1452,6 @@ impl super::PlatformImpl for Manager { self.device_generation, )?; let ring_info = pad.ring.as_mut()?; - println!("Looking for {}", ring_info.axis.index); let raw_valuator_value = valuator_fetch(ring_info.axis.index)?; let transformed_valuator_value = ring_info.axis.transform.transform_fixed(raw_valuator_value); From 37f69e2b57afe05d721f121f852bd604cf083dfe Mon Sep 17 00:00:00 2001 From: Aspen Pratt Date: Tue, 24 Sep 2024 16:31:08 -0700 Subject: [PATCH 14/15] biggg rewrite into xi2, undocumented magic strings. this is turning out to not be very "device agnostic", one of the major goals of octotablet. Alas, cursed support is better than no support. --- src/platform/mod.rs | 5 +- src/platform/xinput2/mod.rs | 687 +++++++++++++++++--------------- src/platform/xinput2/strings.rs | 315 +++++++++++++++ 3 files changed, 686 insertions(+), 321 deletions(-) create mode 100644 src/platform/xinput2/strings.rs diff --git a/src/platform/mod.rs b/src/platform/mod.rs index bb2c38e..5e72ada 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -196,11 +196,12 @@ impl From for ButtonID { pub(crate) enum RawEventsIter<'a> { #[cfg(wl_tablet)] Wayland(std::slice::Iter<'a, crate::events::raw::Event>), - #[cfg(wl_tablet)] + #[cfg(xinput2)] XInput2(std::slice::Iter<'a, crate::events::raw::Event>), #[cfg(ink_rts)] Ink(std::slice::Iter<'a, crate::events::raw::Event>), - // Prevent error when no backends are available. + // Prevent unused lifetime error when no backends are available. + #[allow(dead_code)] Uninhabited(&'a std::convert::Infallible), } impl Iterator for RawEventsIter<'_> { diff --git a/src/platform/xinput2/mod.rs b/src/platform/xinput2/mod.rs index 631a1eb..30370b2 100644 --- a/src/platform/xinput2/mod.rs +++ b/src/platform/xinput2/mod.rs @@ -1,3 +1,5 @@ +use core::str; + use crate::events::raw; use x11rb::{ connection::{Connection, RequestConnection}, @@ -7,8 +9,16 @@ use x11rb::{ }, }; +mod strings; + /// If this many milliseconds since last ring interaction, emit an Out event. const RING_TIMEOUT_MS: Timestamp = 200; +const XI_ANY_PROPERTY_TYPE: u32 = 0; + +/// Maximum number of groups to try and query from libinput devices. +/// I'm unaware of devices with >2 but a larger number does not hurt. +const MAX_GROUPS: u32 = 4; + /// If this many milliseconds since last tool interaction, emit an Out event. const TOOL_TIMEOUT_MS: Timestamp = 500; // Some necessary constants not defined by x11rb: @@ -16,33 +26,9 @@ const XI_ALL_DEVICES: u16 = 0; const XI_ALL_MASTER_DEVICES: u16 = 1; /// Magic timestamp signalling to the server "now". const NOW_MAGIC: x11rb::protocol::xproto::Timestamp = 0; -// Strings are used to communicate the class of device, so we need a hueristic to -// find devices we are interested in and a transformation to a more well-documented enum. -// Strings used here are defined in X11/extensions/XI.h -/// X "device_type" atom for [`crate::tablet`]s... -const TYPE_TABLET: &str = "TABLET"; -/// .. [`crate::pad`]s... -const TYPE_PAD: &str = "PAD"; -/// .. [`crate::pad`]s also ?!?!?... -const TYPE_TOUCHPAD: &str = "TOUCHPAD"; -/// ..stylus tips.. -const TYPE_STYLUS: &str = "STYLUS"; -/// and erasers! -const TYPE_ERASER: &str = "ERASER"; -// Type "xwayland-pointer" is used for xwayland mice, styluses, erasers and... *squint* ...keyboards? -// The role could instead be parsed from it's user-facing device name: -// "xwayland-tablet-pad:" -// "xwayland-tablet eraser:" (note the hyphen becomes a space) -// "xwayland-tablet stylus:" -// Which is unfortunately a collapsed stream of all devices (similar to X's concept of a Master device) -// and thus all per-device info (names, hardware IDs, capabilities) is lost in abstraction. -const TYPE_XWAYLAND_POINTER: &str = "xwayland-pointer"; const EMULATED_TABLET_NAME: &str = "octotablet emulated"; -const TYPE_MOUSE: &str = "MOUSE"; -const TYPE_TOUCHSCREEN: &str = "TOUCHSCREEN"; - /// Comes from datasize of "button count" field of `ButtonInfo` - button names in xinput are indices, /// with the zeroth index referring to the tool "down" state. pub type ButtonID = std::num::NonZero; @@ -50,90 +36,99 @@ pub type ButtonID = std::num::NonZero; #[derive(Debug, Clone, Copy)] enum ValuatorAxis { // Absolute position, in a normalized device space. - // AbsX, - // AbsY, + AbsX, + AbsY, + AbsDistance, AbsPressure, // Degrees, -,- left and away from user. AbsTiltX, AbsTiltY, + AbsRz, // This pad ring, degrees, and maybe also stylus scrollwheel? I have none to test, // but under Xwayland this capability is listed for both pad and stylus. AbsWheel, } -impl std::str::FromStr for ValuatorAxis { - type Err = (); - fn from_str(axis_label: &str) -> Result { - Ok(match axis_label { - // "Abs X" => Self::AbsX, - // "Abs Y" => Self::AbsY, - "Abs Pressure" => Self::AbsPressure, - "Abs Tilt X" => Self::AbsTiltX, - "Abs Tilt Y" => Self::AbsTiltY, - "Abs Wheel" => Self::AbsWheel, - // My guess is the next one is roll axis, but I do - // not have a any devices that report this axis. - _ => return Err(()), - }) - } -} -impl From for crate::axis::Axis { - fn from(value: ValuatorAxis) -> Self { - match value { +impl TryFrom for crate::axis::Axis { + type Error = (); + fn try_from(value: ValuatorAxis) -> Result { + Ok(match value { + ValuatorAxis::AbsX | ValuatorAxis::AbsY => return Err(()), ValuatorAxis::AbsPressure => Self::Pressure, ValuatorAxis::AbsTiltX | ValuatorAxis::AbsTiltY => Self::Tilt, ValuatorAxis::AbsWheel => Self::Wheel, - //Self::AbsX | Self::AbsY => return None, - } + ValuatorAxis::AbsDistance => Self::Distance, + ValuatorAxis::AbsRz => Self::Roll, + }) + } +} +fn match_valuator_label( + label: u32, + atoms: &strings::xi::axis_label::absolute::Atoms, +) -> Option { + let label = std::num::NonZero::new(label)?; + if label == atoms.x { + Some(ValuatorAxis::AbsX) + } else if label == atoms.y { + Some(ValuatorAxis::AbsY) + } else if label == atoms.distance { + Some(ValuatorAxis::AbsDistance) + } else if label == atoms.pressure { + Some(ValuatorAxis::AbsPressure) + } else if label == atoms.tilt_x { + Some(ValuatorAxis::AbsTiltX) + } else if label == atoms.tilt_y { + Some(ValuatorAxis::AbsTiltY) + } else if label == atoms.rz { + Some(ValuatorAxis::AbsRz) + } else if label == atoms.wheel { + Some(ValuatorAxis::AbsWheel) + } else { + None } } + +#[derive(Copy, Clone)] enum DeviceType { Tool(crate::tool::Type), - Tablet, Pad, } -enum DeviceTypeOrXwayland { - Type(DeviceType), - /// Device type of xwayland-pointer doesn't tell us much, we must - /// also inspect the user-facing device name. - Xwayland, -} -impl std::str::FromStr for DeviceTypeOrXwayland { - type Err = (); - fn from_str(device_type: &str) -> Result { - use crate::tool::Type; - Ok(match device_type { - TYPE_STYLUS => Self::Type(DeviceType::Tool(Type::Pen)), - TYPE_ERASER => Self::Type(DeviceType::Tool(Type::Eraser)), - TYPE_PAD => Self::Type(DeviceType::Pad), - TYPE_TABLET => Self::Type(DeviceType::Tablet), - // TYPE_MOUSE => Self::Tool(Type::Mouse), - TYPE_XWAYLAND_POINTER => Self::Xwayland, - _ => return Err(()), - }) - } + +struct XWaylandDeviceInfo { + ty: DeviceType, + // opaque seat ident. given that wayland identifies seats by string name, the exact + // interpretation of an integer id is unknown to me and i gave up reading the xwayland + // implementation lol. + seat: u32, } /// Parse the device name of an xwayland device, where the type is stored. -/// Use if [`DeviceType`] parsing came back as `DeviceType::Xwayland`. -fn xwayland_type_from_name(device_name: &str) -> Option { +fn parse_xwayland_from_name(device_name: &str) -> Option { use crate::tool::Type; - let class = device_name.strip_prefix("xwayland-tablet")?; - // there is a numeric field at the end, unclear what it means. - // For me, it's *always* `:43`, /shrug! - let colon = class.rfind(':')?; + use strings::xwayland; + let class = device_name.strip_prefix(xwayland::NAME_PREFIX)?; + // there is a numeric field at the end, which seems to be an opaque + // representation of the wayland seat the cursor belongs to. + // weirdly, they behave as several children to one master instead of many masters. + let colon = class.rfind(xwayland::NAME_SEAT_SEPARATOR)?; let class = &class[..colon]; - - Some(match class { - // Weird inconsistent prefix xP - "-pad" => DeviceType::Pad, - " stylus" => DeviceType::Tool(Type::Pen), - " eraser" => DeviceType::Tool(Type::Eraser), + let seat: u32 = class + .get((std::ops::Bound::Excluded(colon), std::ops::Bound::Unbounded)) + .and_then(|seat| seat.parse().ok())?; + + let class = match class { + xwayland::NAME_PAD_SUFFIX => DeviceType::Pad, + xwayland::NAME_STYLUS_SUFFIX => DeviceType::Tool(Type::Pen), + xwayland::NAME_ERASER_SUFFIX => DeviceType::Tool(Type::Eraser), + // Lenses and mice get coerced to this same xwayland ident.. darn. + xwayland::NAME_MOUSE_LENS_SUFFIX => DeviceType::Tool(Type::Mouse), _ => return None, - }) + }; + + Some(XWaylandDeviceInfo { ty: class, seat }) } #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub(super) enum ID { +pub enum ID { /// Special value for the emulated tablet. This is an invalid ID for tools and pads. /// A bit of an API design whoopsie! EmulatedTablet, @@ -146,36 +141,48 @@ pub(super) enum ID { }, } -#[derive(Copy, Clone)] +#[derive(Clone)] struct ToolName<'a> { - /// The friendly form of the name, minus ID code. - human_readable: &'a str, /// The tablet name we expect to own this tool - maybe_associated_tablet: Option<&'a str>, + maybe_associated_tablet: Option>, /// The hardware serial of the tool. id: Option, + /// The expected type of the device + device_type: Option, + /// The xwayland seat associated with the device. + xwayland_seat: Option, } impl<'a> ToolName<'a> { - fn human_readable(self) -> &'a str { - self.human_readable - } - fn id(self) -> Option { + fn id(&self) -> Option { self.id } - fn maybe_associated_tablet(self) -> Option<&'a str> { - self.maybe_associated_tablet + fn maybe_associated_tablet(&self) -> Option<&str> { + self.maybe_associated_tablet.as_deref() } } /// From the user-facing Device name, try to parse several tool fields. -fn parse_tool_name(name: &str) -> ToolName { - // X11 seems to place tool hardware IDs within the human-readable Name of the device, and this is +fn guess_from_name(name: &str) -> ToolName { + // soem drivers place tool hardware IDs within the human-readable Name of the device, and this is // the only place it is exposed. Predictably, as with all things X, this is not documented as far // as I can tell. - // From experiments, it consists of the [tablet name][tool type string][hex number (or zero) - // in parentheses] - This is a hueristic and likely non-exhaustive, for example it does not apply to xwayland. + // Some drivers, input-wacom and input-libinput, also expose this through device properties. + + // From experiments, it tends to consist of the [tablet name][tool type string][hex number (or zero) + // in parentheses] - This is a hueristic and non-exhaustive, for example it does not apply to xwayland. + + // xwayland has a fixed format, check for that before we get all hueristic-y. + if let Some(xwayland) = parse_xwayland_from_name(name) { + return ToolName { + device_type: Some(xwayland.ty), + id: None, + maybe_associated_tablet: None, + xwayland_seat: Some(xwayland.seat), + }; + } + // Get the numeric ID, plus the string minus that id. let try_parse_id = || -> Option<(&str, crate::tool::HardwareID)> { // Detect the range of characters within the last set of parens. let open_paren = name.rfind('(')?; @@ -203,36 +210,51 @@ fn parse_tool_name(name: &str) -> ToolName { Some((name_text, crate::tool::HardwareID(id_num))) }; + // May be none. this is not a failure. let id_parse_result = try_parse_id(); - let (human_readable, id) = match id_parse_result { + // Take the string part minus id, if any. + let (text_part, id) = match id_parse_result { Some((name, id)) => (name, Some(id)), None => (name, None), }; - let try_parse_maybe_associated_tablet = || -> Option<&str> { - // Hueristic, of course. These are the only two kinds of hardware I have to test with, - // unsure how e.g. an airbrush would register. - if let Some(tablet_name) = human_readable.strip_suffix(" Pen") { - return Some(tablet_name); - } - if let Some(tablet_name) = human_readable.strip_suffix(" Eraser") { - return Some(tablet_name); - } - None + // .. and try to parse remaining two properties. + let try_parse_tablet_name_and_ty = || -> Option<(std::borrow::Cow<'_, str>, DeviceType)> { + // Tend to be named "tablet name" + "tool type" + let last_space = text_part.rfind(' ')?; + let last_word = text_part.get(last_space.checked_add(1)?..)?; + let mut tablet_name = std::borrow::Cow::Borrowed(&text_part[..last_space]); + + let ty = match last_word { + // this can totally be a false positive! Eg, my intuos is called + // "Intuos S Pen" and the stylus is called "Intuos S Pen Pen (0xblahblah)". + "pen" | "Pen" | "stylus" | "Stylus" => DeviceType::Tool(crate::tool::Type::Pen), + "eraser" | "Eraser" => DeviceType::Tool(crate::tool::Type::Eraser), + // Mouse or Lens device get coerced to this same label. + "cursor" | "Cursor" => DeviceType::Tool(crate::tool::Type::Mouse), + "pad" | "Pad" => { + // Pads break the pattern of suffix removal, weirdly. + tablet_name = std::borrow::Cow::Owned(tablet_name.into_owned() + " Pen"); + DeviceType::Pad + } + // "Finger" | "finger" => todo!(), + // "Touch" | "touch" => todo!(), + _ => return None, + }; + + Some((tablet_name, ty)) }; + let (tablet_name, ty) = try_parse_tablet_name_and_ty().unzip(); + ToolName { - human_readable, - maybe_associated_tablet: try_parse_maybe_associated_tablet(), + maybe_associated_tablet: tablet_name, + device_type: ty, + xwayland_seat: None, id, } } -fn pad_maybe_associated_tablet(name: &str) -> Option { - // Hueristic, of course. - name.strip_suffix(" Pad") - .map(|prefix| prefix.to_owned() + " Pen") -} /// Turn an xinput fixed-point number into a float, rounded. // I could probably keep them fixed for more maths, but this is easy for right now. fn fixed32_to_f32(fixed: xinput::Fp3232) -> f32 { @@ -250,11 +272,18 @@ fn fixed32_to_f32(fixed: xinput::Fp3232) -> f32 { } /// Turn an xinput fixed-point number into a float, rounded. // I could probably keep them fixed for more maths, but this is easy for right now. -fn fixed16_to_f32(fixed: i32) -> f32 { +fn fixed16_to_f32(fixed: xinput::Fp1616) -> f32 { // Could bit-twiddle these into place instead, likely with more precision. (fixed as f32) / 65536.0 } +#[derive(Copy, Clone)] +struct WacomIDs { + hardware_serial: u32, + hardware_id: u32, + tablet_id: u32, +} + #[derive(Copy, Clone)] enum Transform { BiasScale { bias: f32, scale: f32 }, @@ -288,7 +317,9 @@ enum Phase { /// Contains the metadata for translating a device's events to octotablet events, /// as well as the x11 specific state required to emulate certain events. struct ToolInfo { + distance: Option, pressure: Option, + roll: Option, tilt: [Option; 2], wheel: Option, /// The tablet this tool belongs to, based on heuristics. @@ -462,7 +493,7 @@ pub struct Manager { tablets: Vec, events: Vec>, window: x11rb::protocol::xproto::Window, - atom_usb_id: Option>, + atoms: strings::Atoms, // What is the most recent event timecode? server_time: Timestamp, /// Device ID generation. Increment when one or more devices is removed in a frame. @@ -478,14 +509,9 @@ impl Manager { conn.extension_information(xinput::X11_EXTENSION_NAME) .unwrap() .unwrap(); - /*let version = conn - // What the heck is "name"? it is totally undocumented and is not part of the XLib interface. - // I was unable to reverse engineer it, it seems to work regardless of what data is given to it. - .xinput_get_extension_version(b"Fixme!") - .unwrap() - .reply() - .unwrap();*/ + let version = conn.xinput_xi_query_version(2, 4).unwrap().reply().unwrap(); + println!( "Server supports v{}.{}", version.major_version, version.minor_version @@ -493,16 +519,6 @@ impl Manager { assert!(version.major_version >= 2); - // conn.xinput_select_extension_event( - // window, - // // Some crazy logic involving the output of OpenDevice. - // // /usr/include/X11/extensions/XInput.h has the macros that do the crime, however it seems nonportable to X11rb. - // // https://www.x.org/archive/X11R6.8.2/doc/XGetSelectedExtensionEvents.3.html - // &[u32::from(xinput::CHANGE_DEVICE_NOTIFY_EVENT)], - // ) - // .unwrap() - // .check() - // .unwrap(); let hierarchy_interest = xinput::EventMask { deviceid: XI_ALL_DEVICES, mask: [ @@ -535,13 +551,8 @@ impl Manager { .check() .unwrap();*/ - let atom_usb_id = conn - .intern_atom(false, b"Device Product ID") - .ok() - .and_then(|resp| resp.reply().ok()) - .and_then(|reply| reply.atom.try_into().ok()); - let mut this = Self { + atoms: strings::intern(&conn).unwrap(), conn, tool_infos: std::collections::BTreeMap::new(), pad_infos: std::collections::BTreeMap::new(), @@ -550,7 +561,6 @@ impl Manager { events: vec![], tablets: vec![], window, - atom_usb_id, server_time: 0, device_generation: 0, }; @@ -576,94 +586,134 @@ impl Manager { // Pad ids to bulk-enable events on. let mut pad_listen_events = vec![]; - // Okay, this is weird. There are two very similar functions, xi_query_device and list_input_devices. - // The venne diagram of the data contained within their responses is nearly a circle, however each - // has subtle differences such that we need to query both and join the data. >~<; - - // "Clients are requested to avoid mixing XI1.x and XI2 code as much as possible" well then maybe - // you shoulda made query_device actually return all the necessary data ya silly goober. - let device_queries = self + // Device detection strategy: + // * Look for wacom-specific type field. + // * If found, gather up all the wacom-specific properties. + // * Not so "device agnostic" anymore, are ya, octotablet? :pensive: + // * Look for xwayland name + // * Look for generic tablet-like names ("foobar Stylus/Eraser/Pad" and the matching "foobar" device) + // * If found, try to also parse the hardware ID, which is often found as hexx in parenthesis at the end. + // * Will likely ident tablets as tools, rely on the fact that tablets don't have valuators to filter. + // * Look for stylus-like capabilities + // * Abs X, Y OR Abs + // * This *will* falsely detect other devices, like pads. perhaps we will have to wait for an + // event to determine whether it's a pad or tool. + let device_infos = self .conn .xinput_xi_query_device(XI_ALL_DEVICES) .unwrap() .reply() - .unwrap(); - - let device_list = self - .conn - .xinput_list_input_devices() .unwrap() - .reply() - .unwrap(); + .infos; - // We recieve axis infos in a flat list, into which the individual devices refer. - // (mutable slice as we'll trim it as we consume) - let mut flat_infos = &device_list.infos[..]; - // We also recieve name strings in a parallel list. - for (name, device) in device_list - .names - .into_iter() - .zip(device_list.devices.into_iter()) - { + for device in &device_infos { // Zero is a special value (ALL_DEVICES), and can't be used by a device. - let nonzero_id = std::num::NonZero::new(u16::from(device.device_id)).unwrap(); + let nonzero_id = std::num::NonZero::new(device.deviceid).unwrap(); let octotablet_id = ID::ID { generation: self.device_generation, device_id: nonzero_id, }; - - let _infos = { - // Split off however many axes this device claims. - let (infos, tail_infos) = flat_infos.split_at(device.num_class_info.into()); - flat_infos = tail_infos; - infos - }; - // Find the query that represents this device. - // Query and list contain very similar info, but both have tiny extra nuggets that we - // need. - let Some(query) = device_queries - .infos - .iter() - .find(|qdevice| qdevice.deviceid == u16::from(device.device_id)) - else { - continue; - }; - - // Query the "type" atom, which will describe what this device actually is through some heuristics. - // We can't use the capabilities it advertises as our detection method, since a lot of them are - // nonsensical (pad reporting absolute x,y, pressure, etc - but it doesn't do anything!) - if device.device_type == 0 { - // None. + // Only look at enabled pointer devices. + // Pads tend to list as pointers under several drivers, hmm. + // Tablets are keyboards + if !device.enabled || !matches!(device.type_, xinput::DeviceType::SLAVE_POINTER) { continue; } - let Some(device_type) = self + + // Try to pase xf86-input-wacom driver-specific fields. + // First is device type (used to be standard in XI 1 but + // 2 removed it without a replacement?!?) + let wacom_type = self .conn - // This is *not* cached. Should we? We expect a small set of valid values, - // but on the other hand this isn't exactly a hot path. - .get_atom_name(device.device_type) + .xinput_xi_get_property( + device.deviceid, + false, + self.atoms.wacom.prop_tool_type.get(), + XI_ANY_PROPERTY_TYPE, + 0, + 1, + ) + .unwrap() + .reply() .ok() - // Whew! - .and_then(|response| response.reply().ok()) - .and_then(|atom| String::from_utf8(atom.name).ok()) - .and_then(|type_stirng| type_stirng.parse::().ok()) - else { - continue; + .and_then(|repl| { + let card32 = *repl.items.as_data32()?.first()?; + let ty = std::num::NonZero::new(card32)?; + let wacom = &self.atoms.wacom; + + if ty == wacom.type_stylus { + Some(DeviceType::Tool(crate::tool::Type::Pen)) + } else if ty == wacom.type_eraser { + Some(DeviceType::Tool(crate::tool::Type::Eraser)) + } else if ty == wacom.type_pad { + Some(DeviceType::Pad) + } else if ty == wacom.type_cursor { + Some(DeviceType::Tool(crate::tool::Type::Mouse)) + } else { + None + } + }); + // Then hardware id and tablet association. For other drivers we will parse this + // from name, but wacom driver gives an actual solution lol. + let wacom_ids = if let Some( + &[tablet_id, old_serial, old_hardware_id, _new_serial, _new_hardware_id], + ) = self + .conn + .xinput_xi_get_property( + device.deviceid, + false, + self.atoms.wacom.prop_serial_ids.get(), + XI_ANY_PROPERTY_TYPE, + 0, + 5, + ) + .unwrap() + .reply() + .ok() + .as_ref() + .and_then(|repl| repl.items.as_data32()) + // as_deref doesn't work here?? lol + .map(Vec::as_slice) + { + // Tablet_id is an opaque identifier, not comparable with xinput's deviceid. + // However, since xf86-input-wacom removes tablet devices from the listing, it + // still may be valuable to divide into several emulated tablet devices. + + // old_serial is the tool's serial identifier, if applicable. matches with + // wayland tablet_v2 serial~! I am unsure the difference between old and new serial, + // other than that the new_* values are zeroed when the tool is out of proximity. + + Some(WacomIDs { + tablet_id, + hardware_serial: old_serial, + hardware_id: old_hardware_id, + }) + } else { + None }; // UTF8 human-readable device name, which encodes some additional info sometimes. - let raw_name = String::from_utf8(name.name).ok(); - let device_type = match device_type { - DeviceTypeOrXwayland::Type(t) => t, - // Generic xwayland type, parse the device name to find type instead. - DeviceTypeOrXwayland::Xwayland => { - let Some(ty) = raw_name.as_deref().and_then(xwayland_type_from_name) else { - // Couldn't figure out what the device is.. - continue; - }; - ty - } + let raw_name = String::from_utf8_lossy(&device.name); + // Apply heaps of heuristics to figure out what the heck this device is all about + let name_fields = guess_from_name(&raw_name); + + // Combine our knowledge. Trust the wacom driver type over guessed type. + // If we couldn't determine, then we can't use the device. + // todo: determine from Classes. + let Some(device_type) = wacom_type.or(name_fields.device_type) else { + continue; }; + let hardware_id = wacom_ids + .map(|ids| crate::tool::HardwareID(ids.hardware_serial.into())) + .or(name_fields.id); + + // If we cant find an xi device for the tablet, this is useful to differentiate + // the tablets. + let opaque_tablet_id = wacom_ids + .map(|ids| ids.tablet_id) + .or(name_fields.xwayland_seat); + // At this point, we're pretty sure this is a tool, pad, or tablet! match device_type { @@ -676,53 +726,46 @@ impl Manager { // but it behaves weird when not grabbed and it's not easy to know // when to grab/release a floating device. // (We could manually implement a hit test? yikes) - if query.type_ != xinput::DeviceType::SLAVE_POINTER { + if device.type_ != xinput::DeviceType::SLAVE_POINTER { continue; } - // Try to parse the hardware ID from the name field. - let name_fields = raw_name.as_deref().map(parse_tool_name); - - let tablet_id = name_fields - .and_then(ToolName::maybe_associated_tablet) - .and_then(|expected| { - // Find the device with the expected name, and return it's ID if found. - let tablet_info = device_queries - .infos - .iter() - .find(|info| info.name == expected.as_bytes())?; - - Some(ID::ID { - generation: self.device_generation, - // 0 is a special value, this is infallible. - device_id: tablet_info.deviceid.try_into().unwrap(), - }) - }); + let tablet_id = name_fields.maybe_associated_tablet().and_then(|expected| { + // Find the device with the expected name, and return it's ID if found. + let tablet_info = device_infos + .iter() + .find(|info| info.name == expected.as_bytes())?; + + Some(ID::ID { + generation: self.device_generation, + // 0 is a special value, this is infallible. + device_id: tablet_info.deviceid.try_into().unwrap(), + }) + }); let mut octotablet_info = crate::tool::Tool { internal_id: super::InternalID::XInput2(octotablet_id), - name: name_fields - .map(ToolName::human_readable) - .map(ToOwned::to_owned), - hardware_id: name_fields.and_then(ToolName::id), - wacom_id: None, + name: Some(raw_name.clone().into_owned()), + hardware_id, + wacom_id: wacom_ids.map(|ids| ids.hardware_id.into()), tool_type: Some(ty), axes: crate::axis::FullInfo::default(), }; let mut x11_info = ToolInfo { pressure: None, + distance: None, + roll: None, tilt: [None, None], wheel: None, tablet: tablet_id.unwrap_or(ID::EmulatedTablet), phase: Phase::Out, - master_pointer: query.attachment, - master_keyboard: device_queries - .infos + master_pointer: device.attachment, + master_keyboard: device_infos .iter() .find_map(|q| { // Find the info for the master pointer - if q.deviceid == query.attachment { + if q.deviceid == device.attachment { // Look at the master pointer's attachment, // which is the associated master keyboard's ID. Some(q.attachment) @@ -737,7 +780,7 @@ impl Manager { }; // Look for axes! - for class in &query.classes { + for class in &device.classes { if let Some(v) = class.data.as_valuator() { if v.mode != xinput::ValuatorMode::ABSOLUTE { continue; @@ -746,13 +789,8 @@ impl Manager { if v.min == v.max { continue; } - let Some(label) = self - .conn - .get_atom_name(v.label) - .ok() - .and_then(|response| response.reply().ok()) - .and_then(|atom| String::from_utf8(atom.name).ok()) - .and_then(|label| label.parse::().ok()) + let Some(label) = + match_valuator_label(v.label, &self.atoms.absolute_axes) else { continue; }; @@ -761,6 +799,7 @@ impl Manager { let max = fixed32_to_f32(v.max); match label { + ValuatorAxis::AbsX | ValuatorAxis::AbsY => (), ValuatorAxis::AbsPressure => { // Scale and bias to [0,1]. x11_info.pressure = Some(AxisInfo { @@ -773,6 +812,34 @@ impl Manager { octotablet_info.axes.pressure = Some(crate::axis::NormalizedInfo { granularity: None }); } + ValuatorAxis::AbsDistance => { + // Scale and bias to [0,1]. + x11_info.distance = Some(AxisInfo { + index: v.number, + transform: Transform::BiasScale { + bias: -min, + scale: 1.0 / (max - min), + }, + }); + octotablet_info.axes.distance = + Some(crate::axis::LengthInfo::Normalized( + crate::axis::NormalizedInfo { granularity: None }, + )); + } + ValuatorAxis::AbsRz => { + // Scale and bias to [0,TAU). + // This may be biased to the wrong 0 angle, but octotablet + // doesn't make hard guarantees about it anyway. + x11_info.roll = Some(AxisInfo { + index: v.number, + transform: Transform::BiasScale { + bias: -min, + scale: std::f32::consts::TAU / (max - min), + }, + }); + octotablet_info.axes.roll = + Some(crate::axis::CircularInfo { granularity: None }); + } ValuatorAxis::AbsTiltX => { // Seemingly always in degrees. let deg_to_rad = 1.0f32.to_radians(); @@ -851,18 +918,18 @@ impl Manager { } } - tool_listen_events.push(u16::from(device.device_id)); + tool_listen_events.push(device.deviceid); self.tools.push(octotablet_info); self.tool_infos.insert(octotablet_id, x11_info); } DeviceType::Pad => { - if query.type_ == xinput::DeviceType::FLOATING_SLAVE { + if device.type_ == xinput::DeviceType::FLOATING_SLAVE { // We need master attachments in order to grab. continue; } let mut buttons = 0; let mut ring_axis = None; - for class in &query.classes { + for class in &device.classes { match &class.data { xinput::DeviceClassData::Button(b) => { buttons = b.num_buttons(); @@ -874,13 +941,8 @@ impl Manager { } // This fails to detect xwayland's Ring axis, since it is present but not labeled. // However, in my testing, it's borked anyways and always returns position 71. - let Some(label) = self - .conn - .get_atom_name(v.label) - .ok() - .and_then(|response| response.reply().ok()) - .and_then(|atom| String::from_utf8(atom.name).ok()) - .and_then(|label| label.parse::().ok()) + let Some(label) = + match_valuator_label(v.label, &self.atoms.absolute_axes) else { continue; }; @@ -931,25 +993,21 @@ impl Manager { groups: vec![group], }); - pad_listen_events.push(u16::from(device.device_id)); + pad_listen_events.push(device.deviceid); // Find the tablet this belongs to. - let tablet = raw_name - .as_deref() - .and_then(pad_maybe_associated_tablet) - .and_then(|expected| { - // Find the device with the expected name, and return it's ID if found. - let tablet_info = device_queries - .infos - .iter() - .find(|info| info.name == expected.as_bytes())?; - - Some(ID::ID { - generation: self.device_generation, - // 0 is ALL_DEVICES, this is infallible. - device_id: tablet_info.deviceid.try_into().unwrap(), - }) - }); + let tablet = name_fields.maybe_associated_tablet().and_then(|expected| { + // Find the device with the expected name, and return it's ID if found. + let tablet_info = device_infos + .iter() + .find(|info| info.name == expected.as_bytes())?; + + Some(ID::ID { + generation: self.device_generation, + // 0 is ALL_DEVICES, this is infallible. + device_id: tablet_info.deviceid.try_into().unwrap(), + }) + }); self.pad_infos.insert( octotablet_id, @@ -961,54 +1019,45 @@ impl Manager { tablet: tablet.unwrap_or(ID::EmulatedTablet), }, ); - } - DeviceType::Tablet => { - // Tablets are of... dubious usefulness in xinput? - // They do not follow the paradigms needed by octotablet. - // Alas, we can still fetch some useful information! - let usb_id = self - .conn - // USBID consists of two 16 bit integers, [vid, pid]. - .xinput_xi_get_property( - device.device_id, - false, - self.atom_usb_id.map_or(0, std::num::NonZero::get), - 0, - 0, - 2, - ) - .ok() - .and_then(|resp| resp.reply().ok()) - .and_then(|property| { - #[allow(clippy::get_first)] - // Try to accept any type. - Some(match property.items { - xinput::XIGetPropertyItems::Data16(d) => crate::tablet::UsbId { - vid: *d.get(0)?, - pid: *d.get(1)?, - }, - xinput::XIGetPropertyItems::Data8(d) => crate::tablet::UsbId { - vid: (*d.get(0)?).into(), - pid: (*d.get(1)?).into(), - }, - xinput::XIGetPropertyItems::Data32(d) => crate::tablet::UsbId { - vid: (*d.get(0)?).try_into().ok()?, - pid: (*d.get(1)?).try_into().ok()?, - }, - xinput::XIGetPropertyItems::InvalidValue(_) => return None, - }) - }); - - // We can also fetch device path here. - - let tablet = crate::tablet::Tablet { - internal_id: super::InternalID::XInput2(octotablet_id), - name: raw_name, - usb_id, - }; - - self.tablets.push(tablet); - } + } /* + DeviceType::Tablet => { + // Tablets are of... dubious usefulness in xinput? + // They do not follow the paradigms needed by octotablet. + // Alas, we can still fetch some useful information! + let usb_id = self + .conn + // USBID consists of two 32 bit integers, [vid, pid]. + .xinput_xi_get_property( + device.deviceid, + false, + self.atoms.xi.prop_product_id.get(), + XI_ANY_PROPERTY_TYPE, + 0, + 2, + ) + .ok() + .and_then(|resp| resp.reply().ok()) + .and_then(|property| { + let Some(&[vid, pid]) = property.items.as_data32().map(Vec::as_slice) + else { + return None; + }; + Some(crate::tablet::UsbId { + vid: (vid).try_into().ok()?, + pid: (pid).try_into().ok()?, + }) + }); + + // We can also fetch device path here. + + let tablet = crate::tablet::Tablet { + internal_id: super::InternalID::XInput2(octotablet_id), + name: Some(raw_name.into_owned()), + usb_id, + }; + + self.tablets.push(tablet); + }*/ } } diff --git a/src/platform/xinput2/strings.rs b/src/platform/xinput2/strings.rs new file mode 100644 index 0000000..dda8c5f --- /dev/null +++ b/src/platform/xinput2/strings.rs @@ -0,0 +1,315 @@ +//! Magic strings used by the XI implementation as well as certain driver implementations. +//! +//! More are defined than are actually used, mostly to remind my future self that the option +//! exists. + +pub type Atom = std::num::NonZero; + +/// Interned atoms. For documentation on values and use, see the other modules in [`super::strings`]. +pub struct Atoms { + pub wacom: wacom::Atoms, + pub libinput: libinput::Atoms, + pub xi: xi::Atoms, + pub absolute_axes: xi::axis_label::absolute::Atoms, +} + +#[derive(Debug, thiserror::Error)] +pub enum InternError { + #[error(transparent)] + Connection(#[from] x11rb::errors::ConnectionError), + #[error(transparent)] + Reply(#[from] x11rb::errors::ReplyError), + #[error("server replied with null atom")] + NullReply, +} + +/// Intern all the needed atoms. +pub fn intern(conn: Conn) -> Result +where + Conn: x11rb::connection::RequestConnection + x11rb::protocol::xproto::ConnectionExt, +{ + use xi::axis_label::absolute; + // Reasoning - if no device has been connected to prompt the driver to appear and register it's atoms, + // we still want to be able to see them upon attachment without restarting the octotablet client.. right? + // On the other hand, I'm not really sure if drivers are even lazily loaded. + const ONLY_IF_EXISTS: bool = false; + + // Bulk request, then bulk recv. Makes the protocol latency O(1) instead of O(n). Not that it matters, + // this is one-time setup code xP + // xlib has a bulk-intern call, x11rb does not. + + let intern = |name: &str| -> Result< + x11rb::cookie::Cookie<'_, Conn, x11rb::protocol::xproto::InternAtomReply>, + x11rb::errors::ConnectionError, + > { conn.intern_atom(ONLY_IF_EXISTS, name.as_bytes()) }; + + // wacom + let prop_tool_type = intern(wacom::PROP_TOOL_TYPE)?; + let type_stylus = intern(wacom::TYPE_STYLUS)?; + let type_cursor = intern(wacom::TYPE_CURSOR)?; + let type_eraser = intern(wacom::TYPE_ERASER)?; + let type_pad = intern(wacom::TYPE_PAD)?; + let prop_serial_ids = intern(wacom::PROP_SERIALIDS)?; + + // libinput + let prop_tool_serial = intern(libinput::PROP_TOOL_SERIAL)?; + let prop_tool_id = intern(libinput::PROP_TOOL_ID)?; + let prop_pad_group_modes_available = intern(libinput::PROP_PAD_GROUP_MODES_AVAILABLE)?; + let prop_pad_group_current_modes = intern(libinput::PROP_PAD_GROUP_CURRENT_MODES)?; + let prop_pad_button_groups = intern(libinput::PROP_PAD_BUTTON_GROUPS)?; + let prop_pad_strip_groups = intern(libinput::PROP_PAD_STRIP_GROUPS)?; + let prop_pad_ring_groups = intern(libinput::PROP_PAD_RING_GROUPS)?; + + // xi + let prop_product_id = intern(xi::PROP_PRODUCT_ID)?; + let prop_device_node = intern(xi::PROP_DEVICE_NODE)?; + + // xi standard absolute axis labels + let x = intern(absolute::PROP_X)?; + let y = intern(absolute::PROP_Y)?; + let rz = intern(absolute::PROP_RZ)?; + let distance = intern(absolute::PROP_DISTANCE)?; + let pressure = intern(absolute::PROP_PRESSURE)?; + let tilt_x = intern(absolute::PROP_TILT_X)?; + let tilt_y = intern(absolute::PROP_TILT_Y)?; + let wheel = intern(absolute::PROP_WHEEL)?; + + let parse_reply = |atom: x11rb::cookie::Cookie< + Conn, + x11rb::protocol::xproto::InternAtomReply, + >| + -> Result { + atom.reply()? + .atom + .try_into() + .map_err(|_| InternError::NullReply) + }; + + Ok(Atoms { + wacom: wacom::Atoms { + prop_tool_type: parse_reply(prop_tool_type)?, + type_stylus: parse_reply(type_stylus)?, + type_cursor: parse_reply(type_cursor)?, + type_eraser: parse_reply(type_eraser)?, + type_pad: parse_reply(type_pad)?, + prop_serial_ids: parse_reply(prop_serial_ids)?, + }, + libinput: libinput::Atoms { + prop_tool_serial: parse_reply(prop_tool_serial)?, + prop_tool_id: parse_reply(prop_tool_id)?, + + prop_pad_group_modes_available: parse_reply(prop_pad_group_modes_available)?, + prop_pad_group_current_modes: parse_reply(prop_pad_group_current_modes)?, + prop_pad_button_groups: parse_reply(prop_pad_button_groups)?, + prop_pad_strip_groups: parse_reply(prop_pad_strip_groups)?, + prop_pad_ring_groups: parse_reply(prop_pad_ring_groups)?, + }, + xi: xi::Atoms { + prop_product_id: parse_reply(prop_product_id)?, + prop_device_node: parse_reply(prop_device_node)?, + }, + absolute_axes: xi::axis_label::absolute::Atoms { + x: parse_reply(x)?, + y: parse_reply(y)?, + rz: parse_reply(rz)?, + distance: parse_reply(distance)?, + pressure: parse_reply(pressure)?, + tilt_x: parse_reply(tilt_x)?, + tilt_y: parse_reply(tilt_y)?, + wheel: parse_reply(wheel)?, + }, + }) +} + +/// Definitions from the xf86-input-wacom driver: +/// +/// +/// See also +/// which seems to imply there's a strong, pre-determined ordering of valuators. Hmf. This matches +/// with what I have seen in the wild, but it feels like the wrong solution to rely on that...? +pub mod wacom { + /// value is an atom, equal to one of the `TYPE_*` values. + /// This is a replacement for the deprecated "type" atom that used to exist in XI 1 + pub const PROP_TOOL_TYPE: &str = "Wacom Tool Type"; + pub const TYPE_STYLUS: &str = "STYLUS"; + pub const TYPE_CURSOR: &str = "CURSOR"; + pub const TYPE_ERASER: &str = "ERASER"; + pub const TYPE_PAD: &str = "PAD"; + pub const TYPE_TOUCH: &str = "TOUCH"; + + /// CARD32[5], tablet id, old serial, old hw id, new serial, new hw id. + /// idk what old and new means. experimentally new is 0 when out and =old when in. + /// + /// "old serial" matches up exactly with the value from wayland's tablet-v2, so + /// it seems like that's our guy! :D + pub const PROP_SERIALIDS: &str = "Wacom Serial IDs"; + + pub struct Atoms { + pub prop_tool_type: super::Atom, + pub type_stylus: super::Atom, + pub type_cursor: super::Atom, + pub type_eraser: super::Atom, + pub type_pad: super::Atom, + pub prop_serial_ids: super::Atom, + } +} + +/// Definitions from the xf86-input-libinput driver: +/// +pub mod libinput { + /// Hardware ID, u32. If exists and is zero, it has no ID. + pub const PROP_TOOL_SERIAL: &str = "libinput Tablet Tool Serial"; + /// Vendor-specific fine-grain hardware type, u32. Corresponds to [`crate::tool::Tool::wacom_id`]. I can't find a listing + /// of these! + /// + /// See also: + pub const PROP_TOOL_ID: &str = "libinput Tablet Tool ID"; + // The following have been renamed to use octotablet verbage (group instead of mode group) + // (at this point we are just using the X server as a mediator to talk to libinput through hidden channels lmao) + /// CARD8[num groups], number of modes per group. + pub const PROP_PAD_GROUP_MODES_AVAILABLE: &str = "libinput Pad Mode Groups Modes Available"; + /// CARD8[num groups], current mode in `[0, MODES_AVAILABLE)`. + pub const PROP_PAD_GROUP_CURRENT_MODES: &str = "libinput Pad Mode Groups Modes"; + /// INT8[num buttons], associated group for each button, or -1 if no association. + pub const PROP_PAD_BUTTON_GROUPS: &str = "libinput Pad Mode Group Buttons"; + /// INT8[num strips], associated group for each strip, or -1 if no association. + // Hm. Octotablet does not support rings/strips not owned by a group. oops? + pub const PROP_PAD_STRIP_GROUPS: &str = "libinput Pad Mode Group Strips"; + /// INT8[num strips], associated group for each ring, or -1 if no association. + pub const PROP_PAD_RING_GROUPS: &str = "libinput Pad Mode Group Rings"; + + pub struct Atoms { + pub prop_tool_serial: super::Atom, + pub prop_tool_id: super::Atom, + pub prop_pad_group_modes_available: super::Atom, + pub prop_pad_group_current_modes: super::Atom, + pub prop_pad_button_groups: super::Atom, + pub prop_pad_strip_groups: super::Atom, + pub prop_pad_ring_groups: super::Atom, + } +} + +/// Constants for parsing xwayland devices. +/// +/// Name consists of: +/// [`NAME_PREFIX`] + [`NAME_PAD_SUFFIX`], [`NAME_ERASER_SUFFIX`], or [`NAME_STYLUS_SUFFIX`] + [`NAME_SEAT_SEPARATOR`] + integral seat id. +pub mod xwayland { + pub const NAME_PREFIX: &str = "xwayland-tablet"; + // Weird inconsistent separator xP + pub const NAME_PAD_SUFFIX: &str = "-pad"; + pub const NAME_ERASER_SUFFIX: &str = " eraser"; + pub const NAME_STYLUS_SUFFIX: &str = " stylus"; + pub const NAME_MOUSE_LENS_SUFFIX: &str = " cursor"; + pub const NAME_SEAT_SEPARATOR: char = ':'; +} + +/// Definitions from the XI internals: +/// +/// +/// These are not, as far as I can tell, publically documented. However, it is necessary +/// to poke at these internals to discover the capabilities of a device. +pub mod xi { + // Device meta + + // One of these, "Coordinate Transformation Matrix," got me really excited that we could take the + // Abx X Y axis values to logical pixel space ourselves, avoiding the client x,y weirdness and + // implement multicursor in client space! (as of now, multiple tablets on the same seat just make + // the cursor vibrate wildly.) + // alas, it is the identity matrix on all devices I've tested, so it's utterly useless... + + /// CARD32[2], [usb VID, usb PID] + pub const PROP_PRODUCT_ID: &str = "Device Product ID"; + /// String, device path. + pub const PROP_DEVICE_NODE: &str = "Device Node"; + + pub struct Atoms { + pub prop_product_id: super::Atom, + pub prop_device_node: super::Atom, + } + + pub mod axis_label { + use super::super::Atom; + /// Relative axes + pub mod relative { + #![allow(dead_code)] + pub const PROP_X: &str = "Rel X"; + pub const PROP_Y: &str = "Rel Y"; + pub const PROP_Z: &str = "Rel Z"; + pub const PROP_RX: &str = "Rel Rotary X"; + pub const PROP_RY: &str = "Rel Rotary Y"; + pub const PROP_RZ: &str = "Rel Rotary Z"; + pub const PROP_HWHEEL: &str = "Rel Horiz Wheel"; + pub const PROP_DIAL: &str = "Rel Dial"; + pub const PROP_WHEEL: &str = "Rel Vert Wheel"; + pub const PROP_MISC: &str = "Rel Misc"; + pub const PROP_VSCROLL: &str = "Rel Vert Scroll"; + pub const PROP_HSCROLL: &str = "Rel Horiz Scroll"; + } + + /// Absolute axes + /// + /// Some examples on how these are used in the wild: + /// * + /// * + /// + /// Notably, Ring2 and Strips are unlabled in both cases. how are you supposed to detect them if the label is null?! + pub mod absolute { + pub const PROP_X: &str = "Abs X"; + pub const PROP_Y: &str = "Abs Y"; + pub const PROP_Z: &str = "Abs Z"; + pub const PROP_RX: &str = "Abs Rotary X"; + pub const PROP_RY: &str = "Abs Rotary Y"; + pub const PROP_RZ: &str = "Abs Rotary Z"; + /// OKAY SO both input-libinput and input-wacom drivers report... *something* important + /// about the airbrush as ABS_THROTTLE. I have no idea what!! + /// From photos, this seems to correspond physically with pressure on a button, which + /// should then logically correspond with octotablet's non-button-pressure axis. Idk. + pub const PROP_THROTTLE: &str = "Abs Throttle"; + pub const PROP_RUDDER: &str = "Abs Rudder"; + pub const PROP_WHEEL: &str = "Abs Wheel"; + pub const PROP_GAS: &str = "Abs Gas"; + pub const PROP_BRAKE: &str = "Abs Brake"; + pub const PROP_HAT0X: &str = "Abs Hat 0 X"; + pub const PROP_HAT0Y: &str = "Abs Hat 0 Y"; + pub const PROP_HAT1X: &str = "Abs Hat 1 X"; + pub const PROP_HAT1Y: &str = "Abs Hat 1 Y"; + pub const PROP_HAT2X: &str = "Abs Hat 2 X"; + pub const PROP_HAT2Y: &str = "Abs Hat 2 Y"; + pub const PROP_HAT3X: &str = "Abs Hat 3 X"; + pub const PROP_HAT3Y: &str = "Abs Hat 3 Y"; + pub const PROP_PRESSURE: &str = "Abs Pressure"; + pub const PROP_DISTANCE: &str = "Abs Distance"; + pub const PROP_TILT_X: &str = "Abs Tilt X"; + pub const PROP_TILT_Y: &str = "Abs Tilt Y"; + pub const PROP_TOOL_WIDTH: &str = "Abs Tool Width"; + pub const PROP_VOLUME: &str = "Abs Volume"; + pub const PROP_MT_TOUCH_MAJOR: &str = "Abs MT Touch Major"; + pub const PROP_MT_TOUCH_MINOR: &str = "Abs MT Touch Minor"; + pub const PROP_MT_WIDTH_MAJOR: &str = "Abs MT Width Major"; + pub const PROP_MT_WIDTH_MINOR: &str = "Abs MT Width Minor"; + pub const PROP_MT_ORIENTATION: &str = "Abs MT Orientation"; + pub const PROP_MT_POSITION_X: &str = "Abs MT Position X"; + pub const PROP_MT_POSITION_Y: &str = "Abs MT Position Y"; + pub const PROP_MT_TOOL_TYPE: &str = "Abs MT Tool Type"; + pub const PROP_MT_BLOB_ID: &str = "Abs MT Blob ID"; + pub const PROP_MT_TRACKING_ID: &str = "Abs MT Tracking ID"; + pub const PROP_MT_PRESSURE: &str = "Abs MT Pressure"; + pub const PROP_MT_DISTANCE: &str = "Abs MT Distance"; + pub const PROP_MT_TOOL_X: &str = "Abs MT Tool X"; + pub const PROP_MT_TOOL_Y: &str = "Abs MT Tool Y"; + pub const PROP_MISC: &str = "Abs Misc"; + + pub struct Atoms { + pub x: super::Atom, + pub y: super::Atom, + /// "Roll" in octotablet. + pub rz: super::Atom, + pub distance: super::Atom, + pub pressure: super::Atom, + pub tilt_x: super::Atom, + pub tilt_y: super::Atom, + pub wheel: super::Atom, + } + } + } +} From a0228962e4cd2e1cd26dd9bf51f4fafb8d5a1087 Mon Sep 17 00:00:00 2001 From: Aspen Pratt Date: Thu, 26 Sep 2024 16:15:58 -0700 Subject: [PATCH 15/15] Better device detection, + driver-specific feats --- src/platform/xinput2/mod.rs | 768 ++++++++++++++++++++++---------- src/platform/xinput2/strings.rs | 9 + 2 files changed, 552 insertions(+), 225 deletions(-) diff --git a/src/platform/xinput2/mod.rs b/src/platform/xinput2/mod.rs index 30370b2..269a2de 100644 --- a/src/platform/xinput2/mod.rs +++ b/src/platform/xinput2/mod.rs @@ -15,9 +15,12 @@ mod strings; const RING_TIMEOUT_MS: Timestamp = 200; const XI_ANY_PROPERTY_TYPE: u32 = 0; -/// Maximum number of groups to try and query from libinput devices. -/// I'm unaware of devices with >2 but a larger number does not hurt. -const MAX_GROUPS: u32 = 4; +/// Maximum number of aspects to try and query from libinput devices. +/// I don't think there's a downside to making these almost arbitrarily large..? +const LIBINPUT_MAX_GROUPS: u32 = 4; +const LIBINPUT_MAX_STRIPS: u32 = 4; +const LIBINPUT_MAX_RINGS: u32 = 4; +const LIBINPUT_MAX_BUTTONS: u32 = 32; /// If this many milliseconds since last tool interaction, emit an Out event. const TOOL_TIMEOUT_MS: Timestamp = 500; @@ -87,14 +90,15 @@ fn match_valuator_label( } } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] enum DeviceType { - Tool(crate::tool::Type), + Tool(Option), Pad, } +#[derive(Debug)] struct XWaylandDeviceInfo { - ty: DeviceType, + device_type: DeviceType, // opaque seat ident. given that wayland identifies seats by string name, the exact // interpretation of an integer id is unknown to me and i gave up reading the xwayland // implementation lol. @@ -117,14 +121,17 @@ fn parse_xwayland_from_name(device_name: &str) -> Option { let class = match class { xwayland::NAME_PAD_SUFFIX => DeviceType::Pad, - xwayland::NAME_STYLUS_SUFFIX => DeviceType::Tool(Type::Pen), - xwayland::NAME_ERASER_SUFFIX => DeviceType::Tool(Type::Eraser), + xwayland::NAME_STYLUS_SUFFIX => DeviceType::Tool(Some(Type::Pen)), + xwayland::NAME_ERASER_SUFFIX => DeviceType::Tool(Some(Type::Eraser)), // Lenses and mice get coerced to this same xwayland ident.. darn. - xwayland::NAME_MOUSE_LENS_SUFFIX => DeviceType::Tool(Type::Mouse), + xwayland::NAME_MOUSE_LENS_SUFFIX => DeviceType::Tool(Some(Type::Mouse)), _ => return None, }; - Some(XWaylandDeviceInfo { ty: class, seat }) + Some(XWaylandDeviceInfo { + device_type: class, + seat, + }) } #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] @@ -141,31 +148,32 @@ pub enum ID { }, } -#[derive(Clone)] -struct ToolName<'a> { - /// The tablet name we expect to own this tool - maybe_associated_tablet: Option>, - /// The hardware serial of the tool. - id: Option, - /// The expected type of the device - device_type: Option, - /// The xwayland seat associated with the device. - xwayland_seat: Option, +#[derive(Debug)] +enum ToolNameFields<'a> { + Xwayland(XWaylandDeviceInfo), + Generic { + /// The tablet name we expect to own this tool or pad + maybe_associated_tablet: Option>, + /// The hardware serial of the tool. + id: Option, + /// The expected type of the device + device_type: Option, + }, } -impl<'a> ToolName<'a> { - fn id(&self) -> Option { - self.id - } - fn maybe_associated_tablet(&self) -> Option<&str> { - self.maybe_associated_tablet.as_deref() +impl ToolNameFields<'_> { + fn device_type(&self) -> Option { + match self { + Self::Xwayland(XWaylandDeviceInfo { device_type, .. }) => Some(*device_type), + Self::Generic { device_type, .. } => *device_type, + } } } - /// From the user-facing Device name, try to parse several tool fields. -fn guess_from_name(name: &str) -> ToolName { - // soem drivers place tool hardware IDs within the human-readable Name of the device, and this is +fn guess_from_name(mut name: &str) -> ToolNameFields { + // some drivers place tool hardware IDs within the human-readable Name of the device, and this is // the only place it is exposed. Predictably, as with all things X, this is not documented as far // as I can tell. + // https://gitlab.freedesktop.org/xorg/driver/xf86-input-libinput/-/blob/master/src/xf86libinput.c?ref_type=heads#2429 // Some drivers, input-wacom and input-libinput, also expose this through device properties. @@ -174,68 +182,87 @@ fn guess_from_name(name: &str) -> ToolName { // xwayland has a fixed format, check for that before we get all hueristic-y. if let Some(xwayland) = parse_xwayland_from_name(name) { - return ToolName { - device_type: Some(xwayland.ty), - id: None, - maybe_associated_tablet: None, - xwayland_seat: Some(xwayland.seat), - }; + return ToolNameFields::Xwayland(xwayland); } - // Get the numeric ID, plus the string minus that id. - let try_parse_id = || -> Option<(&str, crate::tool::HardwareID)> { + // Get the numeric ID, along with the string minus that id. + // This seems to go back to prehistoric versions of xf86-input-libinput. + let mut try_take_id = || -> Option { // Detect the range of characters within the last set of parens. let open_paren = name.rfind('(')?; let after_open_paren = open_paren + 1; // Find the close paren after the last open paren (weird change-of-base-address thing) let close_paren = after_open_paren + name.get(after_open_paren..)?.find(')')?; - // Find the human-readable name content, minus the id field. - let name_text = name[..open_paren].trim_ascii_end(); + // Update the name to this if we determine that it had an ID field. + let name_minus_id = name[..open_paren].trim_ascii_end(); // Find the id field. // id_text is literal '0', or a hexadecimal number prefixed by literal '0x' let id_text = &name[after_open_paren..close_paren]; - let id_num = if id_text == "0" { + if id_text == "0" { // Should this be considered "None"? The XP-PEN DECO-01 reports this value, despite (afaik) // lacking a genuine hardware ID capability. - 0 + // Answer: Yes! https://gitlab.freedesktop.org/xorg/driver/xf86-input-libinput/-/blob/master/include/libinput-properties.h?ref_type=heads#L251 + // This is only for libinput-backed devices, though, hmst. + // The fact that this is "0" and not "0x0" comes from POSIX [sn]printf. + name = name_minus_id; + None } else if let Some(id_text) = id_text.strip_prefix("0x") { - u64::from_str_radix(id_text, 16).ok()? + u64::from_str_radix(id_text, 16).ok().map(|id| { + // map with side effects? i will be ostrisized for my actions. + name = name_minus_id; + crate::tool::HardwareID(id) + }) } else { - return None; - }; - - Some((name_text, crate::tool::HardwareID(id_num))) + // Nothing found, don't trim. + None + } }; // May be none. this is not a failure. - let id_parse_result = try_parse_id(); - - // Take the string part minus id, if any. - let (text_part, id) = match id_parse_result { - Some((name, id)) => (name, Some(id)), - None => (name, None), - }; + let id = try_take_id(); // .. and try to parse remaining two properties. + // Two forms we need to worry about, visible on input-wacom and input-libinput + // [tablet name] + [tool string] + // [tablet name] - " Pen" + " Pad [Pp]ad" + // :V oof let try_parse_tablet_name_and_ty = || -> Option<(std::borrow::Cow<'_, str>, DeviceType)> { + use crate::tool::Type; + // Funny special case, from libinput + if let Some(tablet) = name.strip_suffix(" unknown tool") { + return Some((tablet.into(), DeviceType::Tool(None))); + } // Tend to be named "tablet name" + "tool type" - let last_space = text_part.rfind(' ')?; - let last_word = text_part.get(last_space.checked_add(1)?..)?; - let mut tablet_name = std::borrow::Cow::Borrowed(&text_part[..last_space]); + let last_space = name.rfind(' ')?; + let last_word = name.get(last_space.checked_add(1)?..)?; + let mut tablet_name = std::borrow::Cow::Borrowed(&name[..last_space]); let ty = match last_word { // this can totally be a false positive! Eg, my intuos is called // "Intuos S Pen" and the stylus is called "Intuos S Pen Pen (0xblahblah)". - "pen" | "Pen" | "stylus" | "Stylus" => DeviceType::Tool(crate::tool::Type::Pen), - "eraser" | "Eraser" => DeviceType::Tool(crate::tool::Type::Eraser), - // Mouse or Lens device get coerced to this same label. - "cursor" | "Cursor" => DeviceType::Tool(crate::tool::Type::Mouse), + "pen" | "Pen" | "stylus" | "Stylus" => DeviceType::Tool(Some(Type::Pen)), + "brush" | "Brush" => DeviceType::Tool(Some(Type::Brush)), + "pencil" | "Pencil" => DeviceType::Tool(Some(Type::Pencil)), + "airbrush" | "Airbrush" => DeviceType::Tool(Some(Type::Airbrush)), + "eraser" | "Eraser" => DeviceType::Tool(Some(Type::Eraser)), + // Sometimes Mouse amd Lens devices get coerced to the "Cursor" label. + // "Mouse" obviously falsly identifies random mice as tools, so we filter for + // Mouse devices with RZ axis (libinput source code says all tablet pointers have it!) + "cursor" | "Cursor" | "mouse" | "Mouse" => DeviceType::Tool(Some(Type::Mouse)), + "lens" | "Lens" => DeviceType::Tool(Some(Type::Lens)), "pad" | "Pad" => { // Pads break the pattern of suffix removal, weirdly. - tablet_name = std::borrow::Cow::Owned(tablet_name.into_owned() + " Pen"); + // Try to convert it to " Pen" which is used everywhere else. + // There's no real fallback here, it's hueristic anywayyy + if let Some(name_minus_pad) = tablet_name + .strip_suffix("Pad") + .or_else(|| tablet_name.strip_suffix("pad")) + { + tablet_name = std::borrow::Cow::Owned(name_minus_pad.to_owned() + "Pen"); + } DeviceType::Pad } // "Finger" | "finger" => todo!(), @@ -248,10 +275,9 @@ fn guess_from_name(name: &str) -> ToolName { let (tablet_name, ty) = try_parse_tablet_name_and_ty().unzip(); - ToolName { + ToolNameFields::Generic { maybe_associated_tablet: tablet_name, device_type: ty, - xwayland_seat: None, id, } } @@ -277,11 +303,86 @@ fn fixed16_to_f32(fixed: xinput::Fp1616) -> f32 { (fixed as f32) / 65536.0 } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] struct WacomIDs { hardware_serial: u32, hardware_id: u32, tablet_id: u32, + // This info is fetched, but unfortunately it's subject to TOCTOU bugs. + // is_in: bool, +} +#[derive(Copy, Clone, Debug)] +enum WacomInfo { + Tool { + ty: crate::tool::Type, + ids: Option, + }, + Pad { + ids: Option, + }, + // Does not report tablets. Does report opaque tablet IDs for the other two hw types, + // which can kinda be used to hallucinate what the tablet might've been lol. +} +impl WacomInfo { + /// Get the corresponding device type. + fn device_type(self) -> DeviceType { + match self { + Self::Tool { ty, .. } => DeviceType::Tool(Some(ty)), + Self::Pad { .. } => DeviceType::Pad, + } + } + fn ids(self) -> Option { + match self { + Self::Pad { ids } | Self::Tool { ids, .. } => ids, + } + } + fn hardware_serial(self) -> Option { + self.ids().map(|ids| ids.hardware_serial) + } + fn hardware_id(self) -> Option { + self.ids().map(|ids| ids.hardware_id) + } +} +#[derive(Copy, Clone, Debug)] +struct LibinputToolInfo { + hardware_serial: Option>, + hardware_id: Option, +} +#[derive(Clone, Debug)] +struct LibinputGroupfulPadInfo { + /// Len < 128. As if, lol. + groups: Vec, + strip_associations: Vec>, + ring_associations: Vec>, + /// Okay sooooo... we have no mapping from xinput button idx to + /// hardware idx, so these cannot be used. Unfortunate. + /// They do not line up. xinput lists 11 buttons with all seven spanning + /// the range seemingly randomly, whereas this lists seven. + button_associations: Vec>, +} +#[derive(Copy, Clone, Debug)] +struct LibinputGroupInfo { + num_modes: u8, + /// Beware, subject to TOCTOU bugs. + current_mode: u8, +} +#[derive(Clone, Debug)] +enum LibinputInfo { + Tool(LibinputToolInfo), + GroupfulPad(LibinputGroupfulPadInfo), + /// *Something* libinput, Mouse or keyboard or groupless pad or tablet or... + /// Libinput provides no concrete way to distinguish :< + SomethingElse, +} +impl LibinputInfo { + /// Get the corresponding device type, or None if not known. + fn device_type(&self) -> Option { + match self { + Self::Tool(_) => Some(DeviceType::Tool(None)), + Self::GroupfulPad(_) => Some(DeviceType::Pad), + Self::SomethingElse => None, + } + } } #[derive(Copy, Clone)] @@ -297,6 +398,13 @@ impl Transform { fn transform_fixed(self, value: xinput::Fp3232) -> f32 { self.transform(fixed32_to_f32(value)) } + /// Create a transform that projects `[min, max]` onto `[0.0, new_max]` + fn normalized(min: f32, max: f32, new_max: f32) -> Self { + Self::BiasScale { + bias: -min, + scale: new_max / (max - min), + } + } } #[derive(Copy, Clone)] @@ -607,130 +715,93 @@ impl Manager { .infos; for device in &device_infos { + // Only look at enabled, non-master devices. + if !device.enabled + || matches!( + device.type_, + xinput::DeviceType::MASTER_KEYBOARD | xinput::DeviceType::MASTER_POINTER + ) + { + continue; + } + // Zero is a special value (ALL_DEVICES), and can't be used by a device. - let nonzero_id = std::num::NonZero::new(device.deviceid).unwrap(); + let Some(nonzero_id) = std::num::NonZero::new(device.deviceid) else { + continue; + }; let octotablet_id = ID::ID { generation: self.device_generation, device_id: nonzero_id, }; - // Only look at enabled pointer devices. - // Pads tend to list as pointers under several drivers, hmm. - // Tablets are keyboards - if !device.enabled || !matches!(device.type_, xinput::DeviceType::SLAVE_POINTER) { - continue; - } - // Try to pase xf86-input-wacom driver-specific fields. - // First is device type (used to be standard in XI 1 but - // 2 removed it without a replacement?!?) - let wacom_type = self - .conn - .xinput_xi_get_property( - device.deviceid, - false, - self.atoms.wacom.prop_tool_type.get(), - XI_ANY_PROPERTY_TYPE, - 0, - 1, - ) - .unwrap() - .reply() - .ok() - .and_then(|repl| { - let card32 = *repl.items.as_data32()?.first()?; - let ty = std::num::NonZero::new(card32)?; - let wacom = &self.atoms.wacom; - - if ty == wacom.type_stylus { - Some(DeviceType::Tool(crate::tool::Type::Pen)) - } else if ty == wacom.type_eraser { - Some(DeviceType::Tool(crate::tool::Type::Eraser)) - } else if ty == wacom.type_pad { - Some(DeviceType::Pad) - } else if ty == wacom.type_cursor { - Some(DeviceType::Tool(crate::tool::Type::Mouse)) - } else { - None - } - }); - // Then hardware id and tablet association. For other drivers we will parse this - // from name, but wacom driver gives an actual solution lol. - let wacom_ids = if let Some( - &[tablet_id, old_serial, old_hardware_id, _new_serial, _new_hardware_id], - ) = self - .conn - .xinput_xi_get_property( - device.deviceid, - false, - self.atoms.wacom.prop_serial_ids.get(), - XI_ANY_PROPERTY_TYPE, - 0, - 5, - ) - .unwrap() - .reply() - .ok() - .as_ref() - .and_then(|repl| repl.items.as_data32()) - // as_deref doesn't work here?? lol - .map(Vec::as_slice) - { - // Tablet_id is an opaque identifier, not comparable with xinput's deviceid. - // However, since xf86-input-wacom removes tablet devices from the listing, it - // still may be valuable to divide into several emulated tablet devices. - - // old_serial is the tool's serial identifier, if applicable. matches with - // wayland tablet_v2 serial~! I am unsure the difference between old and new serial, - // other than that the new_* values are zeroed when the tool is out of proximity. - - Some(WacomIDs { - tablet_id, - hardware_serial: old_serial, - hardware_id: old_hardware_id, - }) + // Wacom driver provides a very definitive yes or no to whether that driver is in use. + let wacom = self.try_query_wacom(device.deviceid); + // Libinput is much more fuzzy. Don't bother if wacom was found. + // Technically this is better expressed through Option> but ehhhh + let libinput = if wacom.is_none() { + self.try_query_libinput(device.deviceid) } else { None }; + // Otherwise, we don't immediately fail. other versions of the drivers or other drivers + // entirely (digimend, udev, old libinput) might still be available. + // Fields from above are authoritative, but the information may be gathered by less clean + // means uwu + // UTF8 human-readable device name, which encodes some additional info sometimes. let raw_name = String::from_utf8_lossy(&device.name); - // Apply heaps of heuristics to figure out what the heck this device is all about + // Apply heaps of heuristics to figure out what the heck this device is all about. let name_fields = guess_from_name(&raw_name); - // Combine our knowledge. Trust the wacom driver type over guessed type. + println!("===={raw_name}====\n Wacom - {wacom:#?}\n Libinput - {libinput:#?}\n Heuristic - {name_fields:#?}"); + + // Combine our knowledge. Trusting drivers over guessed. // If we couldn't determine, then we can't use the device. - // todo: determine from Classes. - let Some(device_type) = wacom_type.or(name_fields.device_type) else { + // todo: determine from Classes, which is 10x more unreliable.... + let Some(device_type) = wacom + .map(WacomInfo::device_type) + // None from libinput could still be a groupless pad, fixme. + .or(libinput.as_ref().and_then(LibinputInfo::device_type)) + .or(name_fields.device_type()) + else { continue; }; - let hardware_id = wacom_ids - .map(|ids| crate::tool::HardwareID(ids.hardware_serial.into())) - .or(name_fields.id); - - // If we cant find an xi device for the tablet, this is useful to differentiate - // the tablets. - let opaque_tablet_id = wacom_ids - .map(|ids| ids.tablet_id) - .or(name_fields.xwayland_seat); - // At this point, we're pretty sure this is a tool, pad, or tablet! match device_type { DeviceType::Tool(ty) => { // It's a tool! Parse all relevant infos. + // There are many false posiives, namely mice being detected as tablet pointers and + // tablets being detected as pens. These are filtered based on valuator hueristics. + // We can only handle tools which have a parent. // (and obviously they shouldn't be a keyboard.) // Technically, a floating pointer can work for our needs, - // but it behaves weird when not grabbed and it's not easy to know - // when to grab/release a floating device. - // (We could manually implement a hit test? yikes) + // but we aren't provided with enough info to project Abs X/Abs Y to client logical pixels, + // so rely on the Master's x/y. if device.type_ != xinput::DeviceType::SLAVE_POINTER { continue; } - let tablet_id = name_fields.maybe_associated_tablet().and_then(|expected| { + // These both need a rename at the crate level. + let hardware_id = wacom + .and_then(WacomInfo::hardware_serial) + .or(libinput.as_ref().and_then(|libinput| match libinput { + LibinputInfo::Tool(t) => Some(t.hardware_serial?.get()), + _ => None, + })) + .map(|val| crate::tool::HardwareID(val.into())); + let wacom_id = wacom.and_then(WacomInfo::hardware_id).or(libinput + .as_ref() + .and_then(|libinput| match libinput { + LibinputInfo::Tool(t) => t.hardware_id, + _ => None, + })); + + /*let tablet_id = name_fields.maybe_associated_tablet().and_then(|expected| { // Find the device with the expected name, and return it's ID if found. let tablet_info = device_infos .iter() @@ -741,14 +812,14 @@ impl Manager { // 0 is a special value, this is infallible. device_id: tablet_info.deviceid.try_into().unwrap(), }) - }); + });*/ let mut octotablet_info = crate::tool::Tool { internal_id: super::InternalID::XInput2(octotablet_id), name: Some(raw_name.clone().into_owned()), hardware_id, - wacom_id: wacom_ids.map(|ids| ids.hardware_id.into()), - tool_type: Some(ty), + wacom_id: wacom_id.map(Into::into), + tool_type: ty, axes: crate::axis::FullInfo::default(), }; @@ -758,7 +829,7 @@ impl Manager { roll: None, tilt: [None, None], wheel: None, - tablet: tablet_id.unwrap_or(ID::EmulatedTablet), + tablet: ID::EmulatedTablet, //tablet_id.unwrap_or(ID::EmulatedTablet), phase: Phase::Out, master_pointer: device.attachment, master_keyboard: device_infos @@ -779,6 +850,13 @@ impl Manager { last_interaction: None, }; + // If it is definitively a tool, start as true. + // Otherwise, look for tool-ish aspects. If false at the end, reject. + let mut looks_toolish = wacom + .is_some_and(|wacom| matches!(wacom, WacomInfo::Tool { .. })) + || libinput + .is_some_and(|libinput| matches!(libinput, LibinputInfo::Tool(_))); + // Look for axes! for class in &device.classes { if let Some(v) = class.data.as_valuator() { @@ -798,16 +876,18 @@ impl Manager { let min = fixed32_to_f32(v.min); let max = fixed32_to_f32(v.max); + // Any absolute valuators. + // This excludes relative styluses with no additional features... + // ..pen-shaped-mice? are those a thing? hm. + looks_toolish = true; + match label { ValuatorAxis::AbsX | ValuatorAxis::AbsY => (), ValuatorAxis::AbsPressure => { // Scale and bias to [0,1]. x11_info.pressure = Some(AxisInfo { index: v.number, - transform: Transform::BiasScale { - bias: -min, - scale: 1.0 / (max - min), - }, + transform: Transform::normalized(min, max, 1.0), }); octotablet_info.axes.pressure = Some(crate::axis::NormalizedInfo { granularity: None }); @@ -816,10 +896,7 @@ impl Manager { // Scale and bias to [0,1]. x11_info.distance = Some(AxisInfo { index: v.number, - transform: Transform::BiasScale { - bias: -min, - scale: 1.0 / (max - min), - }, + transform: Transform::normalized(min, max, 1.0), }); octotablet_info.axes.distance = Some(crate::axis::LengthInfo::Normalized( @@ -832,10 +909,11 @@ impl Manager { // doesn't make hard guarantees about it anyway. x11_info.roll = Some(AxisInfo { index: v.number, - transform: Transform::BiasScale { - bias: -min, - scale: std::f32::consts::TAU / (max - min), - }, + transform: Transform::normalized( + min, + max, + std::f32::consts::TAU, + ), }); octotablet_info.axes.roll = Some(crate::axis::CircularInfo { granularity: None }); @@ -913,20 +991,38 @@ impl Manager { } } - // Resolution is.. meaningless, I think. xwayland is the only server I have + // Resolution field is.. meaningless, I think. xwayland is the only server I have // seen that even bothers to fill it out, and even there it's weird. } } + // Picked up on name heuristics, but doesn't have anything that looks like a tool. + if !looks_toolish { + continue; + } + + // Tablet mice always have rotation axis. This filters out standard mice which + // got wrongly picked up by our name hueristics. + if matches!(octotablet_info.tool_type, Some(crate::tool::Type::Mouse)) + && x11_info.roll.is_none() + { + continue; + } tool_listen_events.push(device.deviceid); self.tools.push(octotablet_info); self.tool_infos.insert(octotablet_id, x11_info); } DeviceType::Pad => { - if device.type_ == xinput::DeviceType::FLOATING_SLAVE { - // We need master attachments in order to grab. - continue; - } + let libinput = libinput.as_ref().and_then(|libinput| match libinput { + LibinputInfo::GroupfulPad(groupful) => Some(groupful), + _ => None, + }); + + // Second ring has no label. really stupid. So, the only way to know if there is two is + // if libinput says there is through nonstandard means. :V + let is_dual_ring = + libinput.is_some_and(|info| info.ring_associations.len() >= 2); + let mut buttons = 0; let mut ring_axis = None; for class in &device.classes { @@ -955,10 +1051,11 @@ impl Manager { let max = fixed32_to_f32(v.max); ring_axis = Some(AxisInfo { index: v.number, - transform: Transform::BiasScale { - bias: -min, - scale: std::f32::consts::TAU / (max - min), - }, + transform: Transform::normalized( + min, + max, + std::f32::consts::TAU, + ), }); } } @@ -983,7 +1080,10 @@ impl Manager { buttons: (0..buttons).map(Into::into).collect::>(), feedback: None, internal_id: crate::platform::InternalID::XInput2(octotablet_id), - mode_count: None, + mode_count: libinput + .and_then(|info| Some(info.groups.first()?.num_modes)) + .map(u32::from) + .and_then(std::num::NonZero::new), rings, strips: vec![], }; @@ -996,7 +1096,7 @@ impl Manager { pad_listen_events.push(device.deviceid); // Find the tablet this belongs to. - let tablet = name_fields.maybe_associated_tablet().and_then(|expected| { + /*let tablet = name_fields.maybe_associated_tablet().and_then(|expected| { // Find the device with the expected name, and return it's ID if found. let tablet_info = device_infos .iter() @@ -1007,7 +1107,7 @@ impl Manager { // 0 is ALL_DEVICES, this is infallible. device_id: tablet_info.deviceid.try_into().unwrap(), }) - }); + });*/ self.pad_infos.insert( octotablet_id, @@ -1016,48 +1116,10 @@ impl Manager { axis: ring_axis, last_interaction: None, }), - tablet: tablet.unwrap_or(ID::EmulatedTablet), + tablet: ID::EmulatedTablet, //tablet.unwrap_or(ID::EmulatedTablet), }, ); - } /* - DeviceType::Tablet => { - // Tablets are of... dubious usefulness in xinput? - // They do not follow the paradigms needed by octotablet. - // Alas, we can still fetch some useful information! - let usb_id = self - .conn - // USBID consists of two 32 bit integers, [vid, pid]. - .xinput_xi_get_property( - device.deviceid, - false, - self.atoms.xi.prop_product_id.get(), - XI_ANY_PROPERTY_TYPE, - 0, - 2, - ) - .ok() - .and_then(|resp| resp.reply().ok()) - .and_then(|property| { - let Some(&[vid, pid]) = property.items.as_data32().map(Vec::as_slice) - else { - return None; - }; - Some(crate::tablet::UsbId { - vid: (vid).try_into().ok()?, - pid: (pid).try_into().ok()?, - }) - }); - - // We can also fetch device path here. - - let tablet = crate::tablet::Tablet { - internal_id: super::InternalID::XInput2(octotablet_id), - name: Some(raw_name.into_owned()), - usb_id, - }; - - self.tablets.push(tablet); - }*/ + } } } @@ -1211,6 +1273,257 @@ impl Manager { .check() .unwrap(); } + fn try_query_wacom(&self, deviceid: u16) -> Option { + use crate::tool::Type; + let atoms = &self.atoms.wacom; + // We assume that if this property is missing or malformed then it's not wacom. + let ty = { + let type_atom = self + .conn + .xinput_xi_get_property( + deviceid, + false, + atoms.prop_tool_type.get(), + XI_ANY_PROPERTY_TYPE, + 0, + 1, + ) + .unwrap() + .reply() + .ok()?; + let type_atom = *type_atom.items.as_data32()?.first()?; + let ty = std::num::NonZero::new(type_atom)?; + + if ty == atoms.type_stylus { + DeviceType::Tool(Some(Type::Pen)) + } else if ty == atoms.type_eraser { + DeviceType::Tool(Some(Type::Eraser)) + } else if ty == atoms.type_pad { + DeviceType::Pad + } else if ty == atoms.type_cursor { + DeviceType::Tool(Some(Type::Mouse)) + } else { + return None; + } + }; + + // This one however, we leave optional. + let try_fetch_ids = || -> Option { + let &[ + tablet_id, + old_serial, + old_hardware_id, + /*_new_serial, _new_hardware_id*/ + .. + ] = self + .conn + .xinput_xi_get_property( + deviceid, + false, + atoms.prop_serial_ids.get(), + XI_ANY_PROPERTY_TYPE, + 0, + 3, + ) + .unwrap() + .reply() + .ok()? + .items + .as_data32()? + .as_slice() + else { + return None; + }; + + Some(WacomIDs { + tablet_id, + hardware_id: old_hardware_id, + hardware_serial: old_serial, + }) + }; + + let ids = try_fetch_ids(); + + Some(match ty { + DeviceType::Pad => WacomInfo::Pad { ids }, + DeviceType::Tool(Some(ty)) => WacomInfo::Tool { ty, ids }, + // None tool type is never generated. + _ => unreachable!(), + }) + } + fn try_query_libinput(&self, deviceid: u16) -> Option { + let atoms = &self.atoms.libinput; + + Some(if let Some(tool) = self.try_query_libinput_tool(deviceid) { + LibinputInfo::Tool(tool) + } else if let Some(groupful_pad) = self.try_query_libinput_groupful_pad(deviceid) { + LibinputInfo::GroupfulPad(groupful_pad) + } else { + // Check an always-present property to see if this is even a libinput device. + let is_libinput = self + .conn + .xinput_xi_get_property( + deviceid, + false, + atoms.prop_heartbeat.get(), + XI_ANY_PROPERTY_TYPE, + 0, + 0, + ) + .unwrap() + .reply() + .ok()? + .type_ + // if Type == None atom, property doesn't exist. + != 0; + + if is_libinput { + LibinputInfo::SomethingElse + } else { + return None; + } + }) + } + fn try_query_libinput_tool(&self, deviceid: u16) -> Option { + let atoms = &self.atoms.libinput; + + // ALWAYS present when a libinput tablet tool, thank goodness! Peter Hutterer you have saved me. + // This requires a fairly recent version of the driver, but hopefully fallback device detection will + // make it work still. (namely, the stringified field as part of the name is much much older) + // https://gitlab.freedesktop.org/xorg/driver/xf86-input-libinput/-/blob/master/src/xf86libinput.c?ref_type=heads#L6640 + let hardware_serial = *self + .conn + .xinput_xi_get_property( + deviceid, + false, + atoms.prop_tool_serial.get(), + XI_ANY_PROPERTY_TYPE, + 0, + 1, + ) + .unwrap() + .reply() + .ok()? + .items + .as_data32()? + .first()?; + + // This one is optional, however. + let hardware_id = self + .conn + .xinput_xi_get_property( + deviceid, + false, + atoms.prop_tool_id.get(), + XI_ANY_PROPERTY_TYPE, + 0, + 1, + ) + .unwrap() + .reply() + .ok() + .and_then(|repl| repl.items.as_data32()?.first().copied()); + + Some(LibinputToolInfo { + // Zero is special None value. + hardware_serial: hardware_serial.try_into().ok(), + hardware_id, + }) + } + fn try_query_libinput_groupful_pad(&self, deviceid: u16) -> Option { + let atoms = &self.atoms.libinput; + + let groups = { + let group_modes_available_reply = self + .conn + .xinput_xi_get_property( + deviceid, + false, + atoms.prop_pad_group_modes_available.get(), + XI_ANY_PROPERTY_TYPE, + 0, + // Len, in 4-byte units. + LIBINPUT_MAX_GROUPS.div_ceil(4), + ) + .unwrap() + .reply() + .ok()?; + let num_groups = group_modes_available_reply.num_items; + let group_modes_available = group_modes_available_reply.items.as_data8()?; + let group_current_mode_reply = self + .conn + .xinput_xi_get_property( + deviceid, + false, + atoms.prop_pad_group_current_modes.get(), + XI_ANY_PROPERTY_TYPE, + 0, + // Len, in 4-byte units. + num_groups.div_ceil(4), + ) + .unwrap() + .reply() + .ok()?; + let group_current_mode = group_current_mode_reply.items.as_data8()?; + + group_modes_available + .iter() + // poor behavior if lengths mismatched. That's invalid anyway. + .zip(group_current_mode) + .map(|(&avail, &cur)| LibinputGroupInfo { + current_mode: cur, + num_modes: avail, + }) + .collect::>() + }; + + // Fetch associations from the given property name and max item count. + let fetch_associations = |prop: strings::Atom, max: u32| -> Vec> { + self.conn + .xinput_xi_get_property( + deviceid, + false, + prop.get(), + XI_ANY_PROPERTY_TYPE, + 0, + // Len, in 4-byte units. + max.div_ceil(4), + ) + .unwrap() + .reply() + .ok() + .as_ref() + .and_then(|repl| repl.items.as_data8().map(Vec::as_slice)) + // If not found, empty slice. + .unwrap_or_default() + .iter() + .map(|&association| { + // Signedness is not reported by the reply type system. + // note that association is actually i8, but negatives are None. + #[allow(clippy::cast_possible_wrap)] + if (association as i8).is_negative() || usize::from(association) > groups.len() + { + None + } else { + Some(association) + } + }) + .collect() + }; + + let ring_associations = fetch_associations(atoms.prop_pad_ring_groups, LIBINPUT_MAX_RINGS); + let strip_associations = + fetch_associations(atoms.prop_pad_strip_groups, LIBINPUT_MAX_STRIPS); + let button_associations = + fetch_associations(atoms.prop_pad_button_groups, LIBINPUT_MAX_BUTTONS); + + Some(LibinputGroupfulPadInfo { + groups, + strip_associations, + ring_associations, + button_associations, + }) + } fn parent_left(&mut self, master: u16, time: Timestamp) { // Release tools. for (&id, tool) in &mut self.tool_infos { @@ -1316,6 +1629,11 @@ impl super::PlatformImpl for Manager { continue; } } + Event::XinputProperty(wawa) => { + // Listen to this to determine when an input-wacom tool goes in and out of prox + // (through "new serial/hw id" becoming zeroed) + // and for input-libinput pad mode switch. + } // xwayland fails to emit Leave/Enter when the cursor is warped to/from another window // by a proximity in event. However, it emits a FocusOut/FocusIn for the associated // master keyboard in that case, which we can use to emulate. diff --git a/src/platform/xinput2/strings.rs b/src/platform/xinput2/strings.rs index dda8c5f..fa50a1c 100644 --- a/src/platform/xinput2/strings.rs +++ b/src/platform/xinput2/strings.rs @@ -59,6 +59,7 @@ where let prop_pad_button_groups = intern(libinput::PROP_PAD_BUTTON_GROUPS)?; let prop_pad_strip_groups = intern(libinput::PROP_PAD_STRIP_GROUPS)?; let prop_pad_ring_groups = intern(libinput::PROP_PAD_RING_GROUPS)?; + let prop_heartbeat = intern(libinput::PROP_HEARTBEAT)?; // xi let prop_product_id = intern(xi::PROP_PRODUCT_ID)?; @@ -103,6 +104,8 @@ where prop_pad_button_groups: parse_reply(prop_pad_button_groups)?, prop_pad_strip_groups: parse_reply(prop_pad_strip_groups)?, prop_pad_ring_groups: parse_reply(prop_pad_ring_groups)?, + + prop_heartbeat: parse_reply(prop_heartbeat)?, }, xi: xi::Atoms { prop_product_id: parse_reply(prop_product_id)?, @@ -178,6 +181,10 @@ pub mod libinput { /// INT8[num strips], associated group for each ring, or -1 if no association. pub const PROP_PAD_RING_GROUPS: &str = "libinput Pad Mode Group Rings"; + /// Something defined for all libinput devices, dont care about the meaning. + pub const PROP_HEARTBEAT: &str = "libinput Send Events Mode Enabled Default"; + + #[allow(clippy::struct_field_names)] pub struct Atoms { pub prop_tool_serial: super::Atom, pub prop_tool_id: super::Atom, @@ -186,6 +193,8 @@ pub mod libinput { pub prop_pad_button_groups: super::Atom, pub prop_pad_strip_groups: super::Atom, pub prop_pad_ring_groups: super::Atom, + + pub prop_heartbeat: super::Atom, } }