diff --git a/examples/tablet.rs b/examples/tablet.rs new file mode 100644 index 000000000..33390d605 --- /dev/null +++ b/examples/tablet.rs @@ -0,0 +1,736 @@ +//! An example demonstrating tablets + +use std::collections::HashMap; + +use smithay_client_toolkit::{ + compositor::{CompositorHandler, CompositorState, Surface}, + delegate_compositor, delegate_output, delegate_keyboard, + delegate_registry, delegate_tablet, delegate_seat, delegate_shm, delegate_xdg_shell, + delegate_xdg_window, + output::{OutputHandler, OutputState}, + registry::{ProvidesRegistryState, RegistryState}, + registry_handlers, + seat::{ + keyboard::{KeyEvent, KeyboardHandler, Keysym, Modifiers, RawModifiers}, + tablet_seat, + tablet, + tablet_tool, + Capability, SeatHandler, SeatState, + }, + shell::{ + xdg::{ + window::{Window, WindowConfigure, WindowDecorations, WindowHandler}, + XdgShell, + }, + WaylandSurface, + }, + shm::{slot::{SlotPool, Buffer}, Shm, ShmHandler}, +}; +use wayland_client::{ + globals::registry_queue_init, + protocol::{wl_output, wl_keyboard, wl_region, wl_seat, wl_shm, wl_surface}, + Connection, Dispatch, QueueHandle, + Proxy, +}; +use wayland_protocols::wp::tablet::zv2::client::{ + zwp_tablet_seat_v2::ZwpTabletSeatV2, + zwp_tablet_tool_v2::ZwpTabletToolV2, + zwp_tablet_v2::ZwpTabletV2, + // zwp_tablet_pad_v2::ZwpTabletPadV2, + // zwp_tablet_pad_group_v2::ZwpTabletPadGroupV2, + // zwp_tablet_pad_ring_v2::ZwpTabletPadRingV2, + // zwp_tablet_pad_strip_v2::ZwpTabletPadStripV2, +}; + +const TWO_PI: f32 = 2. * std::f32::consts::PI; + +const BLACK: raqote::Source = raqote::Source::Solid(raqote::SolidSource { r: 0, g: 0, b: 0, a: 255 }); +const WHITE: raqote::Source = raqote::Source::Solid(raqote::SolidSource { r: 255, g: 255, b: 255, a: 255 }); +const DARK_GREEN: raqote::Source = raqote::Source::Solid(raqote::SolidSource { r: 0, g: 102, b: 0, a: 255 }); +const DARK_RED: raqote::Source = raqote::Source::Solid(raqote::SolidSource { r: 153, g: 0, b: 0, a: 255 }); +const HALF_WHITE: raqote::Source = raqote::Source::Solid(raqote::SolidSource { r: 127, g: 127, b: 127, a: 127 }); + +const NO_TIME: &'static str = " "; + +fn main() { + env_logger::init(); + + let conn = Connection::connect_to_env().unwrap(); + + let (globals, mut event_queue) = registry_queue_init(&conn).unwrap(); + let qh = event_queue.handle(); + + let compositor_state = CompositorState::bind(&globals, &qh) + .expect("wl_compositor not available"); + let shm_state = Shm::bind(&globals, &qh).expect("wl_shm not available"); + let xdg_shell_state = XdgShell::bind(&globals, &qh).expect("xdg shell not available"); + + let surface = compositor_state.create_surface(&qh); + + let window = xdg_shell_state.create_window(surface, WindowDecorations::ServerDefault, &qh); + + window.set_title("Tablet drawing"); + window.set_app_id("io.github.smithay.client-toolkit.Tablet"); + window.set_min_size(Some((256, 256))); + + window.commit(); + + let width = 256; + let height = 256; + // Initial size, but it grows automatically as needed. + let pool = SlotPool::new(width as usize * height as usize * 4, &shm_state) + .expect("Failed to create pool"); + + let mut simple_window = SimpleWindow { + registry_state: RegistryState::new(&globals), + seat_state: SeatState::new(&globals, &qh), + output_state: OutputState::new(&globals, &qh), + compositor_state, + shm_state, + _xdg_shell_state: xdg_shell_state, + + exit: false, + width, + height, + window, + keyboard: None, + keyboard_focus: false, + tablet_seat: None, + tablets: HashMap::new(), + tools: HashMap::new(), + buffer: None, + queued_circles: Vec::new(), + redraw_queued: false, + pool, + }; + + while !simple_window.exit { + event_queue.blocking_dispatch(&mut simple_window).unwrap(); + } +} + +struct Tool { + info: tablet_tool::Info, + state: tablet_tool::State, + cursor_surface: Option, + // This demo is currently only hooked up for custom cursors, + // which were a mite easier to implement, more interesting, + // and may be more commonly desired with tablets anyway. + // If you want to use standard theme cursors, + // for now the easiest (but not quite perfect) way is: + // + // 1. Add this field: + // cursor_shape_device: smithay_client_toolkit::reexports::protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::WpCursorShapeDeviceV1, + // + // 2. Populate it from window.seat_state.cursor_shape_manager(qh).get_shape_device_for_tablet_tool(…). + // + // 3. On ProximityIn events, call cursor_shape_device.set_shape(state.proximity?.serial, shape). + // + // In the future, this should be better integrated, like it is with ThemedPointer, + // but for now that’s the general outline. +} + +struct SimpleWindow { + registry_state: RegistryState, + seat_state: SeatState, + output_state: OutputState, + compositor_state: CompositorState, + shm_state: Shm, + _xdg_shell_state: XdgShell, + + exit: bool, + width: u32, + height: u32, + window: Window, + keyboard: Option, + keyboard_focus: bool, + tablet_seat: Option, + tablets: HashMap, + tools: HashMap, + pool: SlotPool, + buffer: Option, + queued_circles: Vec, + redraw_queued: bool, +} + +struct Circle { + x: f32, + y: f32, + radius: f32, + color: raqote::SolidSource, +} + +impl CompositorHandler for SimpleWindow { + fn scale_factor_changed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _surface: &wl_surface::WlSurface, + _new_factor: i32, + ) { + // Not needed for this example. + } + + fn transform_changed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _surface: &wl_surface::WlSurface, + _new_transform: wl_output::Transform, + ) { + // Not needed for this example. + } + + fn frame( + &mut self, + conn: &Connection, + qh: &QueueHandle, + surface: &wl_surface::WlSurface, + time: u32, + ) { + println!("[t={time:10}] draw Frame callback, {} circles to draw", self.queued_circles.len()); + if surface == self.window.wl_surface() { + self.redraw_queued = false; + self.draw_cursors(conn, qh); + self.draw(conn, qh, false); + } + } + + fn surface_enter( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _surface: &wl_surface::WlSurface, + _output: &wl_output::WlOutput, + ) { + // Not needed for this example. + } + + fn surface_leave( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _surface: &wl_surface::WlSurface, + _output: &wl_output::WlOutput, + ) { + // Not needed for this example. + } +} + +impl OutputHandler for SimpleWindow { + fn output_state(&mut self) -> &mut OutputState { + &mut self.output_state + } + + fn new_output( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _output: wl_output::WlOutput, + ) { + } + + fn update_output( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _output: wl_output::WlOutput, + ) { + } + + fn output_destroyed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _output: wl_output::WlOutput, + ) { + } +} + +impl WindowHandler for SimpleWindow { + fn request_close(&mut self, _: &Connection, _: &QueueHandle, _: &Window) { + self.exit = true; + } + + fn configure( + &mut self, + conn: &Connection, + qh: &QueueHandle, + _window: &Window, + configure: WindowConfigure, + _serial: u32, + ) { + let new_width = configure.new_size.0.map(|v| v.get()).unwrap_or(256); + let new_height = configure.new_size.1.map(|v| v.get()).unwrap_or(256); + if self.width != new_width || self.height != new_height || self.buffer.is_none() { + self.width = new_width; + self.height = new_height; + self.init_canvas(); + } + self.draw(conn, qh, true); + } +} + +impl SeatHandler for SimpleWindow { + fn seat_state(&mut self) -> &mut SeatState { + &mut self.seat_state + } + + fn new_seat(&mut self, _conn: &Connection, _qh: &QueueHandle, _seat: wl_seat::WlSeat) { + // TODO: I would have thought tablet seat initialisation should happen here, + // but this doesn’t seem to be called? + // I’m not at all sure I’m structuring this the right way. + panic!("I thought new_seat didn’t get called?"); + } + + fn new_capability( + &mut self, + _conn: &Connection, + qh: &QueueHandle, + seat: wl_seat::WlSeat, + capability: Capability, + ) { + if capability == Capability::Keyboard && self.keyboard.is_none() { + self.keyboard = self.seat_state.get_keyboard(qh, &seat, None).ok(); + } + // FIXME: this doesn’t seem like the right place to put this. + // Where *should* it go? + if self.tablet_seat.is_none() { + self.tablet_seat = self.seat_state.get_tablet_seat(qh, &seat).ok(); + if self.tablet_seat.is_some() { + println!("Created tablet_seat"); + } else { + println!("Compositor does not support tablet events"); + } + } + } + + fn remove_capability( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _seat: wl_seat::WlSeat, + _capability: Capability, + ) { + } + + fn remove_seat(&mut self, _: &Connection, _: &QueueHandle, _: wl_seat::WlSeat) { + // TODO: do we need to release tablet_seat, or will it sort itself out? + } +} + +impl KeyboardHandler for SimpleWindow { + fn enter( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_keyboard::WlKeyboard, + surface: &wl_surface::WlSurface, + _: u32, + _: &[u32], + _: &[Keysym], + ) { + if self.window.wl_surface() == surface { + self.keyboard_focus = true; + } + } + + fn leave( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_keyboard::WlKeyboard, + surface: &wl_surface::WlSurface, + _: u32, + ) { + if self.window.wl_surface() == surface { + self.keyboard_focus = false; + } + } + + fn press_key( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_keyboard::WlKeyboard, + _: u32, + event: KeyEvent, + ) { + match event.keysym { + Keysym::Delete => { + self.clear_canvas(); + self.queued_circles.clear(); + if let Some(buffer) = &self.buffer { + let surface = self.window.wl_surface(); + buffer.attach_to(surface).expect("buffer attach"); + surface.commit(); + } + }, + _ => (), + } + } + + fn release_key( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_keyboard::WlKeyboard, + _: u32, + _: KeyEvent, + ) { + } + + fn update_modifiers( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_keyboard::WlKeyboard, + _: u32, + _: Modifiers, + _: RawModifiers, + _: u32, + ) { + } +} + +impl tablet_seat::Handler for SimpleWindow {} + +impl tablet::Handler for SimpleWindow { + fn info( + &mut self, + _: &Connection, + _: &QueueHandle, + tablet: &ZwpTabletV2, + info: tablet::Info, + ) { + println!("{NO_TIME}tablet.done {}: {:?}", tablet.id(), info); + self.tablets.insert(tablet.clone(), info); + } + + fn removed( + &mut self, + _: &Connection, + _: &QueueHandle, + tablet: &ZwpTabletV2, + ) { + println!("{NO_TIME}tablet.removed {}", tablet.id()); + self.tablets.remove(tablet); + } +} + +impl tablet_tool::Handler for SimpleWindow { + fn info( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + wtool: &ZwpTabletToolV2, + info: tablet_tool::Info, + ) { + println!("{NO_TIME}tablet_tool.done {}: {:?}", wtool.id(), info); + self.tools.insert(wtool.clone(), Tool { + info, + state: tablet_tool::State::new(), + cursor_surface: None, + }); + } + + fn removed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + wtool: &ZwpTabletToolV2, + ) { + println!("{NO_TIME}tablet_tool.removed {}", wtool.id()); + self.tools.remove(wtool); + } + + fn frame( + &mut self, + _conn: &Connection, + qh: &QueueHandle, + wtool: &ZwpTabletToolV2, + events: &[tablet_tool::Event], + ) { + let tool = self.tools.get_mut(wtool).expect("got frame for unknown tool"); + tool.state.ingest_frame(events); + + print!("[t={:10}] tablet_tool.frame ", tool.state.time); + if tool.state.is_in_proximity() { + if tool.state.is_down() { + let pressure = tool.state.pressure_web(&tool.info); + let radius = 2.0 * pressure as f32; + self.queued_circles.push(Circle { + x: tool.state.x as f32, + y: tool.state.y as f32, + radius, + color: raqote::SolidSource { + r: (tool.state.tilt_x + 90.0 / 180.0 * 255.0) as u8, + g: (tool.state.tilt_y + 90.0 / 180.0 * 255.0) as u8, + b: (tool.state.rotation_degrees / 360.0 * 255.0 % 255.0) as u8, + a: if tool.info.supports_slider() { + ((tool.state.slider_position + 65535) as f64 / 131071.0 * 255.0) as u8 + } else { + // Sure, 0 is the neutraal position and all that, + // but semitransparent looks a bit odd, + // so sans slider support, we’ll just go opaque. + // (b being 0 if rotation is not supported is fine.) + 255 + }, + }, + }); + } + + print!("{} x={:7.2} y={:7.2}", + if tool.state.is_down() { "down" } else { "up " }, + tool.state.x, + tool.state.y); + if tool.info.supports_pressure() { + print!(" pressure={:5}", tool.state.pressure); + } + if tool.info.supports_tilt() { + print!(" tilt_x={:5.2} tilt_y={:5.2}", tool.state.tilt_x, tool.state.tilt_y); + } + if tool.info.supports_distance() { + print!(" distance={:5}", tool.state.distance); + } + if tool.info.supports_rotation() { + print!(" rotation={:6.2}", tool.state.rotation_degrees); + } + if tool.info.supports_slider() { + print!(" slider={:6}", tool.state.slider_position); + } + if tool.info.supports_wheel() { + print!(" wheel={:6.2}", tool.state.wheel_degrees); + } + if tool.state.stylus_button_1_pressed { + print!(" button:1"); + } + if tool.state.stylus_button_2_pressed { + print!(" button:2"); + } + if tool.state.stylus_button_3_pressed { + print!(" button:3"); + } + println!(); + } else { + println!("left proximity"); + } + + // Even if the main window has nothing to redraw, + // the cursors probably do, + // and we’re doing only coarse reactivity here, + // so just queue a general redraw. + self.queue_redraw(qh); + } +} + +impl ShmHandler for SimpleWindow { + fn shm_state(&mut self) -> &mut Shm { + &mut self.shm_state + } +} + +impl SimpleWindow { + fn queue_redraw(&mut self, qh: &QueueHandle) { + if !self.redraw_queued { + let surface = self.window.wl_surface(); + // In theory, it might be better to do frame callbacks on cursor surfaces; don’t know. + // But in practice, doing it on the window surface is plenty good enough. + surface.frame(qh, surface.clone()); + // Have to commit to make the frame request. + surface.commit(); + self.redraw_queued = true; + } + } + + pub fn draw(&mut self, _conn: &Connection, _qh: &QueueHandle, force: bool) { + if self.queued_circles.is_empty() && !force { + println!("{NO_TIME}draw Nothing to draw in the window"); + // Nothing needs updating. + // (It was presumably the cursors needing to be updated.) + return; + } + + let buffer = self.buffer.as_ref().unwrap(); + let canvas = self.pool.canvas(buffer).expect("buffer is still active"); + let mut dt = raqote::DrawTarget::from_backing( + self.width as i32, + self.height as i32, + bytemuck::cast_slice_mut(canvas), + ); + + let mut min_x = i32::MAX; + let mut min_y = i32::MAX; + let mut max_x = i32::MIN; + let mut max_y = i32::MIN; + for circle in self.queued_circles.drain(..) { + let mut pb = raqote::PathBuilder::new(); + pb.arc( + circle.x, + circle.y, + circle.radius, + 0., + TWO_PI, + ); + pb.close(); + dt.fill( + &pb.finish(), + &raqote::Source::Solid(circle.color), + &raqote::DrawOptions::new(), + ); + min_x = min_x.min((circle.x - circle.radius).floor() as i32); + min_y = min_y.min((circle.y - circle.radius).floor() as i32); + max_x = max_x.max((circle.x + circle.radius).ceil() as i32); + max_y = max_y.max((circle.y + circle.radius).ceil() as i32); + } + + let surface = self.window.wl_surface(); + if let (Some(width), Some(height)) = (max_x.checked_sub(min_x), max_y.checked_sub(min_y)) { + surface.damage_buffer(min_x, min_y, width, height); + } + buffer.attach_to(surface).expect("buffer attach"); + surface.commit(); + println!("{NO_TIME}draw Finished drawing frame"); + } + + pub fn draw_cursors(&mut self, _conn: &Connection, qh: &QueueHandle) { + for (wtool, tool) in &mut self.tools { + let Some(tablet_tool::Proximity { serial: proximity_in_serial, .. }) = tool.state.proximity + else { continue }; + let width = 58; + let height = 33; + let (buffer, canvas) = self.pool.create_buffer( + width as i32, + height as i32, + width as i32 * 4, + wl_shm::Format::Argb8888 + ).expect("create buffer"); + // https://github.com/Smithay/client-toolkit/issues/488 workaround. + let canvas = &mut canvas[..width as usize * height as usize * 4]; + + let mut dt = raqote::DrawTarget::from_backing( + width as i32, + height as i32, + bytemuck::cast_slice_mut(canvas), + ); + let o = &raqote::DrawOptions::new(); + dt.clear(raqote::SolidSource { r: 0, g: 0, b: 0, a: 0 }); + + // Draw crosshairs, varyinig with pressure and contact state. + { + let mut pb = raqote::PathBuilder::new(); + let radius = 4.0 + 4.0 * tool.state.pressure_web(&tool.info) as f32; + pb.move_to(16.5 , 1.5 ); + pb.line_to(16.5 , 16.5 - radius); + pb.move_to(16.5 , 16.5 + radius); + pb.line_to(16.5 , 31.5 ); + pb.move_to( 1.5 , 16.5 ); + pb.line_to(16.5 - radius, 16.5 ); + pb.move_to(16.5 + radius, 16.5 ); + pb.line_to(31.5 , 16.5 ); + pb.arc(16.5, 16.5, radius, 0.0, TWO_PI); + let path = pb.finish(); + let mut stroke_style = raqote::StrokeStyle { + width: 3.0, + cap: raqote::LineCap::Square, + ..Default::default() + }; + dt.stroke(&path, &HALF_WHITE, &stroke_style, o); + stroke_style.width = 1.0; + dt.stroke(&path, &if tool.state.is_down() { DARK_GREEN } else { DARK_RED }, &stroke_style, o); + } + + // Draw button states, ’cos why not. + { + let y = 27.0; + let mut x = 30.0; + let width = 8.0; + let height = 6.0; + let dx = 10.0; + for pressed in [ + tool.state.stylus_button_1_pressed, + tool.state.stylus_button_2_pressed, + tool.state.stylus_button_3_pressed, + ] { + dt.fill_rect(x, y, width, height, &BLACK, o); + if !pressed { + dt.fill_rect(x + 1.0, y + 1.0, width - 2.0, height - 2.0, &WHITE, o); + } + x += dx; + } + } + + // Could draw more, but you get the idea. + + let cursor_surface = Surface::new(&self.compositor_state, qh).unwrap(); + let cursor_wl_surface = cursor_surface.wl_surface(); + wtool.set_cursor(proximity_in_serial, Some(cursor_wl_surface), 16, 16); + buffer.attach_to(cursor_wl_surface).expect("buffer attach"); + cursor_wl_surface.damage_buffer(0, 0, width as i32, height as i32); + cursor_wl_surface.commit(); + tool.cursor_surface = Some(cursor_surface); + } + } + + /// Initialise the canvas buffer, damaging but not attaching/committing. + /// + /// This should be called whenever the window is resized, too. + fn init_canvas(&mut self) { + let (buffer, canvas) = self.pool + .create_buffer( + self.width as i32, + self.height as i32, + self.width as i32 * 4, + wl_shm::Format::Xrgb8888, + ) + .expect("create buffer"); + // Make everything white. + canvas.fill(0xff); + self.buffer = Some(buffer); + let surface = self.window.wl_surface(); + surface.damage_buffer(0, 0, i32::MAX, i32::MAX); + } + + /// Clear the canvas to white, damaging but not attaching/committing. + fn clear_canvas(&mut self) { + if let Some(buffer) = &self.buffer { + let canvas = self.pool.canvas(buffer).expect("buffer is still active"); + // Make everything white. + canvas.fill(0xff); + let surface = self.window.wl_surface(); + surface.damage_buffer(0, 0, i32::MAX, i32::MAX); + } + } +} + +delegate_compositor!(SimpleWindow); +delegate_output!(SimpleWindow); +delegate_shm!(SimpleWindow); + +delegate_seat!(SimpleWindow); +delegate_keyboard!(SimpleWindow); +delegate_tablet!(SimpleWindow); + +delegate_xdg_shell!(SimpleWindow); +delegate_xdg_window!(SimpleWindow); + +delegate_registry!(SimpleWindow); + +impl ProvidesRegistryState for SimpleWindow { + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } + registry_handlers![OutputState, SeatState,]; +} + +impl Dispatch for SimpleWindow { + fn event( + _: &mut Self, + _: &wl_region::WlRegion, + _: wl_region::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + } +} diff --git a/src/seat/mod.rs b/src/seat/mod.rs index ecdbcc443..a7b985ac4 100644 --- a/src/seat/mod.rs +++ b/src/seat/mod.rs @@ -7,7 +7,7 @@ use std::{ }; use crate::reexports::client::{ - globals::{Global, GlobalList}, + globals::{BindError, Global, GlobalList}, protocol::{wl_pointer, wl_registry::WlRegistry, wl_seat, wl_shm, wl_surface, wl_touch}, Connection, Dispatch, Proxy, QueueHandle, }; @@ -25,11 +25,22 @@ pub mod pointer; pub mod pointer_constraints; pub mod relative_pointer; pub mod touch; +mod tablet_manager; +pub use tablet_manager::TabletManager; +pub mod tablet_seat; +pub mod tablet; +pub mod tablet_tool; +pub mod tablet_pad; use pointer::cursor_shape::CursorShapeManager; use pointer::{PointerData, PointerDataExt, PointerHandler, ThemeSpec, ThemedPointer, Themes}; use touch::{TouchData, TouchDataExt, TouchHandler}; +use wayland_protocols::wp::tablet::zv2::client::{ + zwp_tablet_manager_v2::ZwpTabletManagerV2, + zwp_tablet_seat_v2::ZwpTabletSeatV2, +}; + #[non_exhaustive] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Capability { @@ -65,14 +76,42 @@ pub enum SeatError { pub struct SeatState { // (name, seat) seats: Vec, - cursor_shape_manager_state: CursorShapeManagerState, + cursor_shape_manager_state: ManagerState, + tablet_manager_state: ManagerState, } #[derive(Debug)] -enum CursorShapeManagerState { - NotPresent, +enum ManagerState { + Failed(BindError), Pending { registry: WlRegistry, global: Global }, - Bound(CursorShapeManager), + Bound(T), +} + +impl ManagerState { + fn bind(&mut self, qh: &QueueHandle, version: std::ops::RangeInclusive, f: impl FnOnce(I) -> T) -> Result<&T, BindError> + where + I: Proxy + 'static, + D: Dispatch + 'static, + { + *self = match std::mem::replace(self, ManagerState::Failed(BindError::NotPresent)) { + ManagerState::Pending { registry, global } => { + match crate::registry::bind_one(®istry, &[global], qh, version, GlobalData) { + Ok(bound) => ManagerState::Bound(f(bound)), + Err(e) => ManagerState::Failed(e), + } + } + other => other, + }; + + match self { + ManagerState::Bound(bound) => Ok(bound), + // FIXME: make BindError impl Clone + //ManagerState::Failed(e) => Err(e.clone()), + ManagerState::Failed(BindError::UnsupportedVersion) => Err(BindError::UnsupportedVersion), + ManagerState::Failed(BindError::NotPresent) => Err(BindError::NotPresent), + ManagerState::Pending { .. } => unreachable!(), + } + } } impl SeatState { @@ -80,33 +119,37 @@ impl SeatState { global_list: &GlobalList, qh: &QueueHandle, ) -> SeatState { - let (seats, cursor_shape_manager) = global_list.contents().with_list(|globals| { - let global = globals - .iter() - .find(|global| global.interface == WpCursorShapeManagerV1::interface().name) - .map(|global| CursorShapeManagerState::Pending { - registry: global_list.registry().clone(), - global: global.clone(), - }) - .unwrap_or(CursorShapeManagerState::NotPresent); - - ( - crate::registry::bind_all(global_list.registry(), globals, qh, 1..=7, |id| { - SeatData { - has_keyboard: Arc::new(AtomicBool::new(false)), - has_pointer: Arc::new(AtomicBool::new(false)), - has_touch: Arc::new(AtomicBool::new(false)), - name: Arc::new(Mutex::new(None)), - id, - } - }) - .expect("failed to bind global"), - global, - ) + let mut cursor_shape_manager_state = ManagerState::Failed(BindError::NotPresent); + let mut tablet_manager_state = ManagerState::Failed(BindError::NotPresent); + let seats = global_list.contents().with_list(|globals| { + for global in globals { + if global.interface == WpCursorShapeManagerV1::interface().name { + cursor_shape_manager_state = ManagerState::Pending { + registry: global_list.registry().clone(), + global: global.clone(), + }; + } else if global.interface == ZwpTabletManagerV2::interface().name { + tablet_manager_state = ManagerState::Pending { + registry: global_list.registry().clone(), + global: global.clone(), + }; + } + } + + crate::registry::bind_all(global_list.registry(), globals, qh, 1..=7, |id| { + SeatData { + has_keyboard: Arc::new(AtomicBool::new(false)), + has_pointer: Arc::new(AtomicBool::new(false)), + has_touch: Arc::new(AtomicBool::new(false)), + name: Arc::new(Mutex::new(None)), + id, + } + }) + .expect("failed to bind global") }); let mut state = - SeatState { seats: vec![], cursor_shape_manager_state: cursor_shape_manager }; + SeatState { seats: vec![], cursor_shape_manager_state, tablet_manager_state }; for seat in seats { let data = seat.data::().unwrap().clone(); @@ -245,25 +288,8 @@ impl SeatState { let wl_ptr = seat.get_pointer(qh, pointer_data); - if let CursorShapeManagerState::Pending { registry, global } = - &self.cursor_shape_manager_state - { - self.cursor_shape_manager_state = - match crate::registry::bind_one(registry, &[global.clone()], qh, 1..=1, GlobalData) - { - Ok(bound) => { - CursorShapeManagerState::Bound(CursorShapeManager::from_existing(bound)) - } - Err(_) => CursorShapeManagerState::NotPresent, - } - } - - let shape_device = - if let CursorShapeManagerState::Bound(ref bound) = self.cursor_shape_manager_state { - Some(bound.get_shape_device(&wl_ptr, qh)) - } else { - None - }; + let shape_device = self.cursor_shape_manager(qh).ok() + .map(|csm| csm.get_shape_device(&wl_ptr, qh)); Ok(ThemedPointer { themes: Arc::new(Mutex::new(Themes::new(theme))), @@ -276,6 +302,16 @@ impl SeatState { }) } + pub fn cursor_shape_manager(&mut self, qh: &QueueHandle) + -> Result<&CursorShapeManager, BindError> + where + D: Dispatch, + D: Dispatch, + D: 'static, + { + self.cursor_shape_manager_state.bind(qh, 1..=1, CursorShapeManager::from_existing) + } + /// Creates a touch handle from a seat. /// /// ## Errors @@ -316,6 +352,20 @@ impl SeatState { Ok(seat.get_touch(qh, udata)) } + + /// Get a tablet seat, to gain access to tablets, tools, et cetera. + pub fn get_tablet_seat( + &mut self, + qh: &QueueHandle, + seat: &wl_seat::WlSeat, + ) -> Result + where + D: Dispatch + 'static, + D: Dispatch + 'static, + { + let tm = self.tablet_manager_state.bind(qh, 1..=1, TabletManager::from_existing)?; + Ok(tm.get_tablet_seat(seat, qh)) + } } pub trait SeatHandler: Sized { diff --git a/src/seat/pointer/cursor_shape.rs b/src/seat/pointer/cursor_shape.rs index d6129745a..973b9c4a3 100644 --- a/src/seat/pointer/cursor_shape.rs +++ b/src/seat/pointer/cursor_shape.rs @@ -3,6 +3,7 @@ use cursor_icon::CursorIcon; use crate::globals::GlobalData; use crate::reexports::client::globals::{BindError, GlobalList}; use crate::reexports::client::protocol::wl_pointer::WlPointer; +use crate::reexports::protocols::wp::tablet::zv2::client::zwp_tablet_tool_v2::ZwpTabletToolV2; use crate::reexports::client::{Connection, Dispatch, Proxy, QueueHandle}; use crate::reexports::protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape; use crate::reexports::protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::WpCursorShapeDeviceV1; @@ -40,6 +41,17 @@ impl CursorShapeManager { self.cursor_shape_manager.get_pointer(pointer, queue_handle, GlobalData) } + pub fn get_shape_device_for_tablet_tool( + &self, + tablet_tool: &ZwpTabletToolV2, + queue_handle: &QueueHandle, + ) -> WpCursorShapeDeviceV1 + where + State: Dispatch + 'static, + { + self.cursor_shape_manager.get_tablet_tool_v2(tablet_tool, queue_handle, GlobalData) + } + pub fn inner(&self) -> &WpCursorShapeManagerV1 { &self.cursor_shape_manager } diff --git a/src/seat/tablet.rs b/src/seat/tablet.rs new file mode 100644 index 000000000..38a21f660 --- /dev/null +++ b/src/seat/tablet.rs @@ -0,0 +1,90 @@ +use std::mem; +use std::sync::Mutex; + +use wayland_client::{ + Connection, + Dispatch, + QueueHandle, +}; +use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_v2::{self, ZwpTabletV2}; + +pub trait Handler: Sized { + /// This is fired at the time of the `zwp_tablet_v2.done` event, + /// and collects any preceding `name`, `id` and `path` events into an [`Info`]. + fn info( + &mut self, + conn: &Connection, + qh: &QueueHandle, + tablet: &ZwpTabletV2, + info: Info, + ); + + /// Sent when the tablet has been removed from the system. + /// When a tablet is removed, some tools may be removed. + /// + /// This method is responsible for running `tablet.destroy()`. ← TODO: true or not? + fn removed( + &mut self, + conn: &Connection, + qh: &QueueHandle, + tablet: &ZwpTabletV2, + ); +} + +/// The description of a tablet device. +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct Info { + /// The descriptive name of the tablet device. + pub name: Option, + /// The USB vendor and product IDs for the tablet device. + pub id: Option<(u32, u32)>, + /// System-specific device paths for the tablet. + /// + /// Path format is unspecified. + /// Clients must figure out what to do with them, if they care. + pub paths: Vec, +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct Data { + info: Mutex, +} + +impl Data { + pub fn new() -> Self { + Self { info: Default::default() } + } +} + +impl Dispatch + for super::TabletManager +where + D: Dispatch + Handler, +{ + fn event( + data: &mut D, + tablet: &ZwpTabletV2, + event: zwp_tablet_v2::Event, + udata: &Data, + conn: &Connection, + qh: &QueueHandle, + ) { + let mut guard = udata.info.lock().unwrap(); + match event { + zwp_tablet_v2::Event::Name { name } => guard.name = Some(name), + zwp_tablet_v2::Event::Id { vid, pid } => guard.id = Some((vid, pid)), + zwp_tablet_v2::Event::Path { path } => guard.paths.push(path), + zwp_tablet_v2::Event::Done => { + let info = mem::take(&mut *guard); + drop(guard); + data.info(conn, qh, tablet, info); + }, + zwp_tablet_v2::Event::Removed => { + data.removed(conn, qh, tablet); + }, + _ => unreachable!(), + } + } +} diff --git a/src/seat/tablet_manager.rs b/src/seat/tablet_manager.rs new file mode 100644 index 000000000..2f25a5e38 --- /dev/null +++ b/src/seat/tablet_manager.rs @@ -0,0 +1,95 @@ +use wayland_client::{ + globals::{BindError, GlobalList}, + protocol::wl_seat::WlSeat, + Connection, + Dispatch, + QueueHandle, +}; + +use wayland_protocols::wp::tablet::zv2::client::{ + zwp_tablet_manager_v2::{self, ZwpTabletManagerV2}, + zwp_tablet_seat_v2::ZwpTabletSeatV2, +}; + +use crate::globals::GlobalData; + +#[derive(Debug)] +pub struct TabletManager { + tablet_manager: ZwpTabletManagerV2, +} + +impl TabletManager { + /// Bind `zwp_tablet_manager_v2` global, if it exists + pub fn bind( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result + where + State: Dispatch + 'static, + { + let tablet_manager = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { tablet_manager }) + } + + pub(crate) fn from_existing(tablet_manager: ZwpTabletManagerV2) -> Self { + Self { tablet_manager } + } + + pub fn get_tablet_seat( + &self, + seat: &WlSeat, + qh: &QueueHandle, + ) -> ZwpTabletSeatV2 + where + D: Dispatch + 'static, + { + self.tablet_manager.get_tablet_seat(seat, qh, ()) + } +} + +impl Dispatch + for TabletManager +where + D: Dispatch, +{ + fn event( + _data: &mut D, + _manager: &ZwpTabletManagerV2, + _event: zwp_tablet_manager_v2::Event, + _: &GlobalData, + _conn: &Connection, + _qh: &QueueHandle, + ) { + unreachable!() + } +} + +#[macro_export] +macro_rules! delegate_tablet { + ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { + $crate::reexports::client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + $crate::reexports::protocols::wp::tablet::zv2::client::zwp_tablet_manager_v2::ZwpTabletManagerV2: $crate::globals::GlobalData + ] => $crate::seat::TabletManager); + $crate::reexports::client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + $crate::reexports::protocols::wp::tablet::zv2::client::zwp_tablet_seat_v2::ZwpTabletSeatV2: () + ] => $crate::seat::TabletManager); + $crate::reexports::client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + $crate::reexports::protocols::wp::tablet::zv2::client::zwp_tablet_v2::ZwpTabletV2: $crate::seat::tablet::Data + ] => $crate::seat::TabletManager); + $crate::reexports::client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + $crate::reexports::protocols::wp::tablet::zv2::client::zwp_tablet_tool_v2::ZwpTabletToolV2: $crate::seat::tablet_tool::Data + ] => $crate::seat::TabletManager); + $crate::reexports::client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + $crate::reexports::protocols::wp::tablet::zv2::client::zwp_tablet_pad_v2::ZwpTabletPadV2: $crate::seat::tablet_pad::Data + ] => $crate::seat::TabletManager); + $crate::reexports::client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + $crate::reexports::protocols::wp::tablet::zv2::client::zwp_tablet_pad_group_v2::ZwpTabletPadGroupV2: $crate::seat::tablet_pad::GroupData + ] => $crate::seat::TabletManager); + $crate::reexports::client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + $crate::reexports::protocols::wp::tablet::zv2::client::zwp_tablet_pad_ring_v2::ZwpTabletPadRingV2: $crate::seat::tablet_pad::RingData + ] => $crate::seat::TabletManager); + $crate::reexports::client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + $crate::reexports::protocols::wp::tablet::zv2::client::zwp_tablet_pad_strip_v2::ZwpTabletPadStripV2: $crate::seat::tablet_pad::StripData + ] => $crate::seat::TabletManager); + }; +} diff --git a/src/seat/tablet_pad.rs b/src/seat/tablet_pad.rs new file mode 100644 index 000000000..9b6650b40 --- /dev/null +++ b/src/seat/tablet_pad.rs @@ -0,0 +1,166 @@ +#![allow(unused_variables)] + +// TODO, stub, just enough so it doesn’t crash if a pad is added (hopefully). +// I, Chris Morgan, hooked all the rest of this stuff up, +// but I don’t have a pad, and don’t see any good dummy device for testing, +// and I won’t benefit from this stuff myself anyway. +// It should be straightforward to implement, but there’s a fair bit of surface area. +// Sorry if you wanted it now. +// Offer to buy me a suitable device, and I’ll be interested. + +use wayland_client::{ + event_created_child, + Connection, + Dispatch, + QueueHandle, +}; +use wayland_protocols::wp::tablet::zv2::client::{ + // TODO: zwp_tablet_pad_ring_v2, zwp_tablet_pad_strip_v2, zwp_tablet_pad_group_v2. + zwp_tablet_pad_ring_v2::{self, ZwpTabletPadRingV2}, + zwp_tablet_pad_strip_v2::{self, ZwpTabletPadStripV2}, + zwp_tablet_pad_group_v2::{self, ZwpTabletPadGroupV2, EVT_STRIP_OPCODE, EVT_RING_OPCODE}, + zwp_tablet_pad_v2::{self, ZwpTabletPadV2, EVT_GROUP_OPCODE}, +}; + +#[doc(hidden)] +#[derive(Debug)] +pub struct Data; + +impl Data { + pub fn new() -> Data { Data } +} + +// zwp_tablet_pad_v2 +// Request: set_feedback +// Request: destroy +// Event: group +// Event: path +// Event: buttons +// Event: done +// Event: button +// Event: enter +// Event: leave +// Event: removed +// Enum: button_state + +impl Dispatch + for super::TabletManager +where + D: Dispatch + + Dispatch + //+ Handler + + 'static, +{ + event_created_child!(D, ZwpTabletPadV2, [ + EVT_GROUP_OPCODE => (ZwpTabletPadGroupV2, GroupData), + ]); + + fn event( + data: &mut D, + pad: &ZwpTabletPadV2, + event: zwp_tablet_pad_v2::Event, + udata: &Data, + conn: &Connection, + qh: &QueueHandle, + ) { + log::warn!(target: "sctk", "got tablet pad event, unimplemented"); + } +} + +impl Dispatch + for super::TabletManager +where + D: Dispatch + + Dispatch + + Dispatch + //+ GroupHandler + + 'static, +{ + event_created_child!(D, ZwpTabletPadV2, [ + EVT_RING_OPCODE => (ZwpTabletPadRingV2, RingData), + EVT_STRIP_OPCODE => (ZwpTabletPadStripV2, StripData), + ]); + + fn event( + data: &mut D, + group: &ZwpTabletPadGroupV2, + event: zwp_tablet_pad_group_v2::Event, + udata: &GroupData, + conn: &Connection, + qh: &QueueHandle, + ) { + log::warn!(target: "sctk", "got tablet pad group event, unimplemented"); + } +} + +impl Dispatch + for super::TabletManager +where + D: Dispatch + //+ RingHandler, +{ + fn event( + data: &mut D, + ring: &ZwpTabletPadRingV2, + event: zwp_tablet_pad_ring_v2::Event, + udata: &RingData, + conn: &Connection, + qh: &QueueHandle, + ) { + log::warn!(target: "sctk", "got tablet pad ring event, unimplemented"); + } +} + +impl Dispatch + for super::TabletManager +where + D: Dispatch + //+ StripHandler, +{ + fn event( + data: &mut D, + strip: &ZwpTabletPadStripV2, + event: zwp_tablet_pad_strip_v2::Event, + udata: &StripData, + conn: &Connection, + qh: &QueueHandle, + ) { + log::warn!(target: "sctk", "got tablet pad strip event, unimplemented"); + } +} + +// zwp_tablet_pad_group_v2 +// Request: destroy +// Event: buttons +// Event: ring +// Event: strip +// Event: modes +// Event: done +// Event: mode_switch +#[doc(hidden)] +#[derive(Debug)] +pub struct GroupData; + +// zwp_tablet_pad_ring_v2 +// Request: set_feedback +// Request: destroy +// Event: source +// Event: angle +// Event: stop +// Event: frame +// Enum: source +#[doc(hidden)] +#[derive(Debug)] +pub struct RingData; + +// zwp_tablet_pad_strip_v2 +// Request: set_feedback +// Request: destroy +// Event: source +// Event: position +// Event: stop +// Event: frame +// Enum: source +#[doc(hidden)] +#[derive(Debug)] +pub struct StripData; diff --git a/src/seat/tablet_seat.rs b/src/seat/tablet_seat.rs new file mode 100644 index 000000000..51e444b6a --- /dev/null +++ b/src/seat/tablet_seat.rs @@ -0,0 +1,95 @@ +use wayland_client::{ + event_created_child, + Connection, + Dispatch, + QueueHandle, +}; +use wayland_protocols::wp::tablet::zv2::client::{ + zwp_tablet_seat_v2::{self, ZwpTabletSeatV2, EVT_TABLET_ADDED_OPCODE, EVT_TOOL_ADDED_OPCODE, EVT_PAD_ADDED_OPCODE}, + zwp_tablet_tool_v2::ZwpTabletToolV2, + zwp_tablet_v2::ZwpTabletV2, + zwp_tablet_pad_v2::ZwpTabletPadV2, +}; + +/// Handler for the tablet seat. +/// +/// The `*_added` methods announce the creation of objects before they’re ready for use. +/// If you might have multiple seats and want to associate devices with their tablet seat, +/// then you can implement them, but otherwise you can just leave them blank. +/// +/// What you *actually* care about is the corresponding handler’s `info` method. +/// That’s when it’s ready to use. +#[allow(unused_variables)] // ← For all the trait method arguments +pub trait Handler: Sized { + /// A tablet has been added. + /// + /// [`tablet::Handler::info`](super::tablet::Handler::info) will be called when it is ready for use. + fn tablet_added( + &mut self, + conn: &Connection, + qh: &QueueHandle, + tablet_seat: &ZwpTabletSeatV2, + id: ZwpTabletV2, + ) {} + + /// A tablet tool has been added. + /// + /// [`tablet_tool::Handler::info`](super::tablet_tool::Handler::info) will be called when it is ready for use. + fn tool_added( + &mut self, + conn: &Connection, + qh: &QueueHandle, + tablet_seat: &ZwpTabletSeatV2, + id: ZwpTabletToolV2, + ) {} + + /// A tablet pad has been added. + /// + /// [`tablet_pad::Handler::info`](super::tablet_pad::Handler::info) will be called when it is ready for use. + fn pad_added( + &mut self, + conn: &Connection, + qh: &QueueHandle, + tablet_seat: &ZwpTabletSeatV2, + id: ZwpTabletPadV2, + ) {} +} + +impl Dispatch + for super::TabletManager +where + D: Dispatch + + Dispatch + + Dispatch + + Dispatch + + Handler + + 'static, +{ + event_created_child!(D, ZwpTabletSeatV2, [ + EVT_TABLET_ADDED_OPCODE => (ZwpTabletV2, super::tablet::Data::new()), + EVT_TOOL_ADDED_OPCODE => (ZwpTabletToolV2, super::tablet_tool::Data::new()), + EVT_PAD_ADDED_OPCODE => (ZwpTabletPadV2, super::tablet_pad::Data::new()), + ]); + + fn event( + data: &mut D, + tablet_seat: &ZwpTabletSeatV2, + event: zwp_tablet_seat_v2::Event, + _udata: &(), + conn: &Connection, + qh: &QueueHandle, + ) { + match event { + zwp_tablet_seat_v2::Event::TabletAdded { id } => { + data.tablet_added(conn, qh, tablet_seat, id); + }, + zwp_tablet_seat_v2::Event::ToolAdded { id } => { + data.tool_added(conn, qh, tablet_seat, id); + }, + zwp_tablet_seat_v2::Event::PadAdded { id } => { + data.pad_added(conn, qh, tablet_seat, id); + }, + _ => unreachable!(), + } + } +} diff --git a/src/seat/tablet_tool.rs b/src/seat/tablet_tool.rs new file mode 100644 index 000000000..f5c83b5b8 --- /dev/null +++ b/src/seat/tablet_tool.rs @@ -0,0 +1,563 @@ +use std::fmt; +use std::mem; +use std::sync::Mutex; + +use wayland_backend::smallvec::SmallVec; +use wayland_client::{ + protocol::wl_surface::WlSurface, + Connection, + Dispatch, + QueueHandle, + Proxy, + WEnum, +}; +use wayland_protocols::wp::tablet::zv2::client::{ + zwp_tablet_tool_v2::{self, ZwpTabletToolV2}, + zwp_tablet_v2::ZwpTabletV2, + // TODO: zwp_tablet_pad_ring_v2, zwp_tablet_pad_strip_v2, zwp_tablet_pad_group_v2. + //zwp_tablet_pad_v2::{self, ZwpTabletPadV2}, +}; + +pub use zwp_tablet_tool_v2::{Capability, Type, Event}; + +// Just a named tuple. +/// A hardware identifier, just two `u32`s with names `hi` and `lo`. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct HardwareSerialOrId { + pub hi: u32, + pub lo: u32, +} + +bitflags::bitflags! { + /// What the tool is capable of, beyond basic X/Y coordinates. + #[derive(Clone, Debug, PartialEq, Eq)] + pub struct Capabilities: u8 { + /// Whether the tool supports tilt. + const TILT = 0b00000001; + /// Whether the tool supports pressure. + const PRESSURE = 0b00000010; + /// Whether the tool can track its distance from the surface. + const DISTANCE = 0b00000100; + /// Whether the tool can measure z-axis rotation. + const ROTATION = 0b00001000; + /// Whether the tool has a slider. + const SLIDER = 0b00010000; + /// Whether the tool has a wheel. + const WHEEL = 0b00100000; + + // Reserve them, but don’t make them part of the public interface. + const _ = 0b01000000; + const _ = 0b10000000; + } +} +const HARDWARE_SERIAL: Capabilities = Capabilities::from_bits_retain(0b01000000); +const HARDWARE_ID_WACOM: Capabilities = Capabilities::from_bits_retain(0b10000000); + +/// Static information about the tool and its capabilities. +#[derive(Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct Info { + // Wish this was #[repr(u8)]… it’s wasting four bytes. + r#type: Type, + // These are really Option<_>, but I squeezed their None discriminant into capabilities, + // as it had two spare bits. This saves eight bytes. You’re welcome. 😛 + hardware_serial: HardwareSerialOrId, + hardware_id_wacom: HardwareSerialOrId, + // Could have used bitflags here—it is already a dep—but we don’t need its complexity. + // Only real loss from this simplicity is meaningful Debug. + capabilities: Capabilities, +} + +// Manual to Option<…> hardware_serial and hardware_id_wacom. +impl fmt::Debug for Info { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Info") + .field("r#type", &self.r#type) + .field("hardware_serial", &self.hardware_serial()) + .field("hardware_id_wacom", &self.hardware_id_wacom()) + .field("capabilities", &self.capabilities) + .finish() + } +} + +impl Default for Info { + fn default() -> Info { + Info { + // I get the impression type is mandatory, + // so this should be overwritten with the correct value. + // But if not… meh, Pen would be the default anyway. + r#type: Type::Pen, + hardware_serial: HardwareSerialOrId { hi: 0, lo: 0 }, + hardware_id_wacom: HardwareSerialOrId { hi: 0, lo: 0 }, + capabilities: Capabilities::empty(), + } + } +} + +impl Info { + /// The type of tool. + pub fn r#type(&self) -> Type { self.r#type } + + /// What the hardware serial number of the tool is, if any. + pub fn hardware_serial(&self) -> Option { + if self.capabilities.contains(HARDWARE_SERIAL) { + Some(self.hardware_serial) + } else { + None + } + } + + /// What the Wacom hardware ID of the tool is, if any. + pub fn hardware_id_wacom(&self) -> Option { + if self.capabilities.contains(HARDWARE_ID_WACOM) { + Some(self.hardware_id_wacom) + } else { + None + } + } + + /// What the tool is capable of, beyond basic X/Y coordinates. + pub fn capabilities(&self) -> Capabilities { self.capabilities.clone() } + /// Whether the tool supports tilt. + pub fn supports_tilt(&self) -> bool { self.capabilities.contains(Capabilities::TILT) } + /// Whether the tool supports pressure. + pub fn supports_pressure(&self) -> bool { self.capabilities.contains(Capabilities::PRESSURE) } + /// Whether the tool can track its distance from the surface. + pub fn supports_distance(&self) -> bool { self.capabilities.contains(Capabilities::DISTANCE) } + /// Whether the tool can measure z-axis rotation. + pub fn supports_rotation(&self) -> bool { self.capabilities.contains(Capabilities::ROTATION) } + /// Whether the tool has a slider. + pub fn supports_slider(&self) -> bool { self.capabilities.contains(Capabilities::SLIDER) } + /// Whether the tool has a wheel. + pub fn supports_wheel(&self) -> bool { self.capabilities.contains(Capabilities::WHEEL) } +} + +/// The current state of the tool. +/// +/// For many applications, when you receive a frame, +/// you don’t so much care about the events, +/// as about capturing the tool’s current total state. +/// +/// This lets you view it that way, if you choose. +/// +/// Caveats: +/// +/// - Although the wheel information is captured as completely as possible here, +/// it should probably be perceived through the event stream; +/// tablet tool wheels are inherently delta-based, +/// so error would accumulate if you try to treat them absolutely. +/// +/// - This only gives a limited view of buttons, +/// only capturing BTN_STYLUS, BTN_STYLUS2 and BTN_STYLUS3 pressed state, +/// not serials or even whether they’ve ever been seen. +/// This is because it makes the interface a good deal nicer, +/// takes less effort to implement efficiently, +/// other buttons are extremely improbable +/// (nothing but stylus inputs have been available since the early-/mid-2010s), +/// and if you care about serials or other buttons you will surely prefer events. +// +// At 184 bytes on a 64-bit platform, this is much larger than it fundamentally *need* be. +// With effort, you could losslessly encode all but wheel in less than 41 bytes, +// and with negligible loss you could shrink it to 33 bytes. +// But we’re trying to make it usefully accessible, not packed ultra-tight. +// So it’s mildly painfully wasteful instead. +#[derive(Debug, Default, Clone)] +pub struct State { + // ProximityIn, ProximityOut + /// The proximity information. + /// + /// When this is `None` (initially and after a `ProximityOut` event), + /// tip state will be up and button states will be released, + /// but all other axes will have stale values. + pub proximity: Option, + + // Down, Up + /// Whether the tool is in logical contact or not. + /// + /// When down, it carries the serial of the last down event. + pub down_serial: Option, + + // Motion + /// The horizontal position, in surface coordinates. + pub x: f64, + /// The vertical position, in surface coordinates. + pub y: f64, + + // Pressure + /// The pressure, scaled from 0–65535, if capable (else 0). + pub pressure: u16, + + // Distance + /// The pressure, scaled from 0–65535, if capable (else 0). + pub distance: u16, + + // Tilt + /// The pen’s tilt in the positive X axis, in degrees (−90 to 90), if capable. + pub tilt_x: f64, + /// The pen’s tilt in the positive X axis, in degrees (−90 to 90), if capable. + pub tilt_y: f64, + + // Rotation + /// The z-axis rotation of the tool, if capable (else 0.0). + /// The rotation value is in degrees clockwise from the tool's logical neutral position. + pub rotation_degrees: f64, + + // Slider + /// The slider position, if capable (else 0). + /// The value is normalized between -65535 and 65535, + /// with 0 as the logical neutral position of the slider. + pub slider_position: i32, + + // Wheel + /// The wheel delta in degrees. + /// + /// See [Event::Wheel] for more information, + /// and guidance on using wheel values. + /// + /// You will probably prefer to consume events, + /// rather than consuming this value. + pub wheel_degrees: f64, + /// The wheel delta in discrete clicks. + /// + /// See [Event::Wheel] for more information, + /// and guidance on using wheel values. + /// + /// You will probably prefer to consume events, + /// rather than consuming this value. + pub wheel_clicks: i32, + + // Button + /// Whether [`BTN_STYLUS`] is pressed. + pub stylus_button_1_pressed: bool, + /// Whether [`BTN_STYLUS2`] is pressed. + pub stylus_button_2_pressed: bool, + /// Whether [`BTN_STYLUS3`] is pressed. + pub stylus_button_3_pressed: bool, + + // Frame + /// The time given in the last [Event::Frame]. + pub time: u32, +} + +/// Information from the `ProximityIn` event. +#[derive(Debug, Clone)] +pub struct Proximity { + /// The `Event::ProximityIn.serial` value, + /// needed for [`ZwpTabletToolV2::set_cursor`] requests. + pub serial: u32, + pub tablet: ZwpTabletV2, + pub surface: WlSurface, +} + +impl State { + /// Create blank state for a tool not yet in proximity. + #[inline] + pub fn new() -> State { + State::default() + } + + /// Whether the tool is in logical contact with the tablet. + #[inline] + pub fn is_down(&self) -> bool { + self.down_serial.is_some() + } + + /// Whether the tool is in proximity to a tablet. + pub fn is_in_proximity(&self) -> bool { + self.proximity.is_some() + } + + /// Apply the events of a frame to this state. + /// + /// `Button` events are ignored for anything other than the stylus buttons, + /// and button serials are discarded. + /// If you want them, you must examine the event stream yourself. + pub fn ingest_frame(&mut self, events: &[Event]) { + for event in events { + match *event { + Event::ProximityIn { serial, ref tablet, ref surface } => { + self.proximity = Some(Proximity { + serial, + tablet: tablet.clone(), + surface: surface.clone(), + }); + }, + Event::ProximityOut => { + self.proximity = None; + }, + Event::Down { serial } => { + self.down_serial = Some(serial); + }, + Event::Up => { + self.down_serial = None; + }, + Event::Motion { x, y } => { + self.x = x; + self.y = y; + }, + Event::Pressure { pressure } => { + // “The value of this event is normalized to a value between 0 and 65535.” + // But the Wayland wire format only supports 32-bit integers, + // so we cast it here. We might as well, I reckon. + self.pressure = pressure as u16; + }, + Event::Distance { distance } => { + // Same deal, “normalized to a value between 0 and 65535”. + self.distance = distance as u16; + }, + Event::Tilt { tilt_x, tilt_y } => { + self.tilt_x = tilt_x; + self.tilt_y = tilt_y; + }, + Event::Rotation { degrees } => { + self.rotation_degrees = degrees; + }, + Event::Slider { position } => { + // This one is “normalized between -65535 and 65535”, 17 bits, so it stays i32. + self.slider_position = position; + }, + Event::Wheel { degrees, clicks } => { + // These ones use += because they’re deltas, unlike the rest. + self.wheel_degrees += degrees; + self.wheel_clicks += clicks; + }, + Event::Button { serial: _, button: BTN_STYLUS, state } => { + self.stylus_button_1_pressed = button_state_to_bool(state); + }, + Event::Button { serial: _, button: BTN_STYLUS2, state } => { + self.stylus_button_2_pressed = button_state_to_bool(state); + }, + Event::Button { serial: _, button: BTN_STYLUS3, state } => { + self.stylus_button_3_pressed = button_state_to_bool(state); + }, + Event::Button { .. } => { + // Deliberately ignored. Very unlikely in 2025, but possible. + }, + Event::Frame { time } => { + self.time = time; + }, + _ => { + // Info events, or anything else unknown. Should be unreachable. + }, + } + } + } + + /// Get the pressure according to the web’s Pointer Events API: + /// scaled in the range \[0.0, 1.0\], + /// and, if pressure isn’t supported, set to 0.5 when down and 0.0 when up. + pub fn pressure_web(&self, info: &Info) -> f64 { + if info.supports_pressure() { + self.pressure as f64 / 65535.0 + } else if self.is_down() { + 0.5 + } else { + 0.0 + } + } + + // Pressure wants a special method, because falling back to 0 would be horrible, + // and there’s an established convention on tip-state-aware fallback, + // and mixing pressure-aware and pressure-unaware pointers is routine. + // (Mind you, within tablet tools they’re probably almost all aware. + // This method is *more* useful when you’ve merged tablet tools, touch and pointers.) + // + // Distance might like a special method, as it’s basically the opposite of pressure, + // distance *from* the surface rather than distance *into* the surface, + // but it’s nowhere near as commonly supported in hardware, + // and it’s not common to build stuff on it, + // and the web’s Pointer Events API doesn’t even expose it, + // so there’s no established convention. + // *Could* make its fallback the opposite of pressure’s, 0.0 if down else 0.5, + // but I think people using distance are likely to want to branch on support, + // a little more than is the case with pressure. + // + // As for the other capabilities, they’re all fine with a static fallback value of zero: + // for tilt, rotation, slider, it’s the natural or neutral position; + // and for wheel, it’s all delta anyway so unsupported is equivalent to unused. +} + +/// Convert an event’s button state to a `bool` representing whether it’s pressed. +#[inline] +pub fn button_state_to_bool(state: WEnum) -> bool { + matches!(state, WEnum::Value(zwp_tablet_tool_v2::ButtonState::Pressed)) +} + +// Based on : +// BTN_STYLUS, BTN_STYLUS2 and BTN_STYLUS3 are the only codes likely. +// Mouse tools are long gone, finger was a mistake—everything’s a stylus. +/// The first button on a stylus. +pub const BTN_STYLUS: u32 = 0x14b; +/// The second button on a stylus. +pub const BTN_STYLUS2: u32 = 0x14c; +/// The third button on a stylus. +pub const BTN_STYLUS3: u32 = 0x149; + +pub trait Handler: Sized { + /// This is fired at the time of the `zwp_tablet_tool_v2.done` event, + /// and collects any preceding `name`, `id` and `path` `type`, `hardware_serial`, + /// `hardware_serial_wacom` and `capability` events into an [`Info`]. + fn info( + &mut self, + conn: &Connection, + qh: &QueueHandle, + tablet: &ZwpTabletToolV2, + info: Info, + ); + + /// Sent when the tablet has been removed from the system. + /// When a tablet is removed, some tools may be removed. + /// + /// This method is responsible for running `tablet.destroy()`. + fn removed( + &mut self, + conn: &Connection, + qh: &QueueHandle, + tablet: &ZwpTabletToolV2, + ); + + /// A series of axis and/or button updates have been received from the tablet. + /// All the events within this frame should be considered one hardware event. + /// The last event in the list passed will always be a `Frame` event, + /// and there will only be that one frame. + fn frame( + &mut self, + conn: &Connection, + qh: &QueueHandle, + tablet: &ZwpTabletToolV2, + events: &[Event], + ); +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct Data { + inner: Mutex, +} + +impl Data { + pub fn new() -> Self { + Self { inner: Default::default() } + } +} + +// I’d make this an enum, but the overhead of keeping info forever is negligible, ~24 bytes. +#[derive(Debug, Default)] +struct DataInner { + /// An accumulation of pending init-time events, flushed when a `done` event comes in, + /// after which it will be perpetually empty. + info: Info, + + /// List of pending events, flushed when a `frame` event comes in. + /// + /// Explanation on chosen inline array sizing: + /// • There will always be at least two events: one axis change, and a Frame. + /// • Three is fundamentally common, when you have proximity and tip events. + /// • Pressure will be almost ubiquitous, so add one for that. + /// • Tilt will be very common too. + /// My opinion, unmeasured save by eyeballing an event stream on a tilt+pressure-capable pen, + /// is that four is probably the sweet spot. + /// Ability to tweak that number would be a good reason to encapsulate this…! + pending: SmallVec<[Event; 4]>, +} + +impl Dispatch + for super::TabletManager +where + D: Dispatch + Handler, +{ + fn event( + data: &mut D, + tool: &ZwpTabletToolV2, + event: zwp_tablet_tool_v2::Event, + udata: &Data, + conn: &Connection, + qh: &QueueHandle, + ) { + let mut guard = udata.inner.lock().unwrap(); + + match event { + + // Initial burst of static description events + // (one Type, + // zero or one HardwareSerial, + // zero or one HardwareIdWacom, + // zero or more Capability, + // then finished with Done). + zwp_tablet_tool_v2::Event::Type { tool_type } => { + guard.info.r#type = match tool_type { + WEnum::Value(tool_type) => tool_type, + WEnum::Unknown(unknown) => { + log::warn!(target: "sctk", "{}: invalid tablet tool type: {:x}", tool.id(), unknown); + return; + }, + }; + }, + zwp_tablet_tool_v2::Event::HardwareSerial { hardware_serial_hi: hi, hardware_serial_lo: lo } => { + guard.info.hardware_serial = HardwareSerialOrId { hi, lo }; + guard.info.capabilities |= HARDWARE_SERIAL; + }, + zwp_tablet_tool_v2::Event::HardwareIdWacom { hardware_id_hi: hi, hardware_id_lo: lo } => { + guard.info.hardware_id_wacom = HardwareSerialOrId { hi, lo }; + guard.info.capabilities |= HARDWARE_ID_WACOM; + }, + zwp_tablet_tool_v2::Event::Capability { capability: WEnum::Value(Capability::Tilt) } => { + guard.info.capabilities |= Capabilities::TILT; + }, + zwp_tablet_tool_v2::Event::Capability { capability: WEnum::Value(Capability::Pressure) } => { + guard.info.capabilities |= Capabilities::PRESSURE; + }, + zwp_tablet_tool_v2::Event::Capability { capability: WEnum::Value(Capability::Distance) } => { + guard.info.capabilities |= Capabilities::DISTANCE; + }, + zwp_tablet_tool_v2::Event::Capability { capability: WEnum::Value(Capability::Rotation) } => { + guard.info.capabilities |= Capabilities::ROTATION; + }, + zwp_tablet_tool_v2::Event::Capability { capability: WEnum::Value(Capability::Slider) } => { + guard.info.capabilities |= Capabilities::SLIDER; + }, + zwp_tablet_tool_v2::Event::Capability { capability: WEnum::Value(Capability::Wheel) } => { + guard.info.capabilities |= Capabilities::WHEEL; + }, + zwp_tablet_tool_v2::Event::Capability { capability: WEnum::Value(_) } => (), + zwp_tablet_tool_v2::Event::Capability { capability: WEnum::Unknown(unknown) } => { + log::warn!(target: "sctk", "{}: invalid tablet tool type: {:x}", tool.id(), unknown); + return; + }, + zwp_tablet_tool_v2::Event::Done => { + let info = mem::take(&mut guard.info); + drop(guard); + data.info(conn, qh, tool, info); + }, + + // Destruction + + zwp_tablet_tool_v2::Event::Removed => { + data.removed(conn, qh, tool); + }, + + // Burst of frame data events + // (one or more of ProximityIn, ProximityOut, Down, Up, Motion, + // Pressure, Distance, Tilt, Rotation, Slider, Wheel, Button, + // with some restrictions on ordering and such; + // then finished with Frame). + + zwp_tablet_tool_v2::Event::Frame { .. } => { + let mut events = mem::take(&mut guard.pending); + drop(guard); + events.push(event); + data.frame(conn, qh, tool, &events); + }, + // Could enumerate all the events, + // but honestly it’s just easier to do this, + // since we’re passing it through, + // not reframing in any way. + // + // Any newly-defined info events will only be fired + // if we bump our declared version support, + // so no concerns there, this will only be the frame events. + _ => guard.pending.push(event), + + } + } +}