diff --git a/Cargo.toml b/Cargo.toml index b48ca8d..5c96f88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ drm-fourcc = "^2.1.1" drm = { version = "0.4.0", optional = true } drm-ffi = { version = "0.1.0", optional = true } gbm = { version = "0.6.0", optional = true, default-features = false, features = ["drm-support"] } -input = { version = "0.5", default-features = false, optional = true } +input = { version = "0.5", default-features = false, features=["libinput_1_14"], optional = true } image = { version = "0.23.14", default-features = false, optional = true } lazy_static = "1" libc = "0.2.70" @@ -57,7 +57,7 @@ backend_gbm = ["gbm"] backend_egl = ["gl_generator"] backend_libinput = ["input"] backend_session = [] -backend_udev = ["udev"] +backend_udev = ["udev", "input/udev"] backend_session_logind = ["dbus", "backend_session", "pkg-config"] backend_session_elogind = ["backend_session_logind"] backend_session_libseat = ["backend_session", "libseat"] diff --git a/anvil/src/input_handler.rs b/anvil/src/input_handler.rs index 2a2b803..07a07c0 100644 --- a/anvil/src/input_handler.rs +++ b/anvil/src/input_handler.rs @@ -8,13 +8,15 @@ use crate::AnvilState; use smithay::{ backend::input::{ - self, Event, InputBackend, InputEvent, KeyState, KeyboardKeyEvent, PointerAxisEvent, - PointerButtonEvent, + self, Device, DeviceCapability, Event, InputBackend, InputEvent, KeyState, KeyboardKeyEvent, + PointerAxisEvent, PointerButtonEvent, ProximityState, TabletToolButtonEvent, TabletToolEvent, + TabletToolProximityEvent, TabletToolTipEvent, TabletToolTipState, }, reexports::wayland_server::protocol::wl_pointer, wayland::{ output::Mode, seat::{keysyms as xkb, AxisFrame, Keysym, ModifiersState}, + tablet_manager::{TabletDescriptor, TabletSeatTrait}, SERIAL_COUNTER as SCOUNTER, }, }; @@ -81,7 +83,7 @@ impl AnvilState { input::MouseButton::Other(b) => b as u32, }; let state = match evt.state() { - input::MouseButtonState::Pressed => { + input::ButtonState::Pressed => { // change the keyboard focus unless the pointer is grabbed if !self.pointer.is_grabbed() { let under = self @@ -93,7 +95,7 @@ impl AnvilState { } wl_pointer::ButtonState::Pressed } - input::MouseButtonState::Released => wl_pointer::ButtonState::Released, + input::ButtonState::Released => wl_pointer::ButtonState::Released, }; self.pointer.button(button, state, serial, evt.time()); } @@ -238,6 +240,29 @@ impl AnvilState { InputEvent::PointerMotion { event, .. } => self.on_pointer_move::(event), InputEvent::PointerButton { event, .. } => self.on_pointer_button::(event), InputEvent::PointerAxis { event, .. } => self.on_pointer_axis::(event), + InputEvent::TabletToolAxis { event, .. } => self.on_tablet_tool_axis::(event), + InputEvent::TabletToolProximity { event, .. } => self.on_tablet_tool_proximity::(event), + InputEvent::TabletToolTip { event, .. } => self.on_tablet_tool_tip::(event), + InputEvent::TabletToolButton { event, .. } => self.on_tablet_button::(event), + InputEvent::DeviceAdded { device } => { + if device.has_capability(DeviceCapability::TabletTool) { + self.seat + .tablet_seat() + .add_tablet(&TabletDescriptor::from(&device)); + } + } + InputEvent::DeviceRemoved { device } => { + if device.has_capability(DeviceCapability::TabletTool) { + let tablet_seat = self.seat.tablet_seat(); + + tablet_seat.remove_tablet(&TabletDescriptor::from(&device)); + + // If there are no tablets in seat we can remove all tools + if tablet_seat.count_tablets() == 0 { + tablet_seat.clear_tools(); + } + } + } _ => { // other events are not handled in anvil (yet) } @@ -259,6 +284,127 @@ impl AnvilState { .motion(self.pointer_location, under, serial, evt.time()); } + fn on_tablet_tool_axis(&mut self, evt: B::TabletToolAxisEvent) { + let output_map = self.output_map.borrow(); + let pointer_location = &mut self.pointer_location; + let tablet_seat = self.seat.tablet_seat(); + let window_map = self.window_map.borrow(); + + output_map + .with_primary(|_, rect| { + pointer_location.0 = evt.x_transformed(rect.width as u32) + rect.x as f64; + pointer_location.1 = evt.y_transformed(rect.height as u32) + rect.y as f64; + + let under = window_map.get_surface_under(*pointer_location); + let tablet = tablet_seat.get_tablet(&TabletDescriptor::from(&evt.device())); + let tool = tablet_seat.get_tool(&evt.tool()); + + if let (Some(tablet), Some(tool)) = (tablet, tool) { + if evt.pressure_has_changed() { + tool.pressure(evt.pressure()); + } + if evt.distance_has_changed() { + tool.distance(evt.distance()); + } + if evt.tilt_has_changed() { + tool.tilt(evt.tilt()); + } + if evt.slider_has_changed() { + tool.slider_position(evt.slider_position()); + } + if evt.rotation_has_changed() { + tool.rotation(evt.rotation()); + } + if evt.wheel_has_changed() { + tool.wheel(evt.wheel_delta(), evt.wheel_delta_discrete()); + } + + tool.motion( + *pointer_location, + under, + &tablet, + SCOUNTER.next_serial(), + evt.time(), + ); + } + }) + .unwrap(); + } + + fn on_tablet_tool_proximity(&mut self, evt: B::TabletToolProximityEvent) { + let output_map = self.output_map.borrow(); + let pointer_location = &mut self.pointer_location; + let tablet_seat = self.seat.tablet_seat(); + let window_map = self.window_map.borrow(); + + output_map + .with_primary(|_, rect| { + let tool = evt.tool(); + tablet_seat.add_tool(&tool); + + pointer_location.0 = evt.x_transformed(rect.width as u32) + rect.x as f64; + pointer_location.1 = evt.y_transformed(rect.height as u32) + rect.y as f64; + + let under = window_map.get_surface_under(*pointer_location); + let tablet = tablet_seat.get_tablet(&TabletDescriptor::from(&evt.device())); + let tool = tablet_seat.get_tool(&tool); + + if let (Some(under), Some(tablet), Some(tool)) = (under, tablet, tool) { + match evt.state() { + ProximityState::In => tool.proximity_in( + *pointer_location, + under, + &tablet, + SCOUNTER.next_serial(), + evt.time(), + ), + ProximityState::Out => tool.proximity_out(evt.time()), + } + } + }) + .unwrap(); + } + + fn on_tablet_tool_tip(&mut self, evt: B::TabletToolTipEvent) { + let tool = self.seat.tablet_seat().get_tool(&evt.tool()); + + if let Some(tool) = tool { + match evt.tip_state() { + TabletToolTipState::Down => { + tool.tip_down(SCOUNTER.next_serial(), evt.time()); + + // change the keyboard focus unless the pointer is grabbed + if !self.pointer.is_grabbed() { + let under = self + .window_map + .borrow_mut() + .get_surface_and_bring_to_top(self.pointer_location); + + let serial = SCOUNTER.next_serial(); + self.keyboard + .set_focus(under.as_ref().map(|&(ref s, _)| s), serial); + } + } + TabletToolTipState::Up => { + tool.tip_up(evt.time()); + } + } + } + } + + fn on_tablet_button(&mut self, evt: B::TabletToolButtonEvent) { + let tool = self.seat.tablet_seat().get_tool(&evt.tool()); + + if let Some(tool) = tool { + tool.button( + evt.button(), + evt.button_state(), + SCOUNTER.next_serial(), + evt.time(), + ); + } + } + fn clamp_coords(&self, pos: (f64, f64)) -> (f64, f64) { if self.output_map.borrow().is_empty() { return pos; diff --git a/anvil/src/state.rs b/anvil/src/state.rs index b197677..fc7068d 100644 --- a/anvil/src/state.rs +++ b/anvil/src/state.rs @@ -16,6 +16,7 @@ use smithay::{ data_device::{default_action_chooser, init_data_device, set_data_device_focus, DataDeviceEvent}, seat::{CursorImageStatus, KeyboardHandle, PointerHandle, Seat, XkbConfig}, shm::init_shm_global, + tablet_manager::{init_tablet_manager_global, TabletSeatTrait}, }, }; @@ -41,6 +42,7 @@ pub struct AnvilState { pub pointer_location: (f64, f64), pub cursor_status: Arc>, pub seat_name: String, + pub seat: Seat, pub start_time: std::time::Instant, // things we must keep alive #[cfg(feature = "xwayland")] @@ -121,6 +123,14 @@ impl AnvilState { *cursor_status2.lock().unwrap() = new_status }); + init_tablet_manager_global(&mut display.borrow_mut()); + + let cursor_status3 = cursor_status.clone(); + seat.tablet_seat().on_cursor_surface(move |_tool, new_status| { + // TODO: tablet tools should have their own cursors + *cursor_status3.lock().unwrap() = new_status; + }); + let keyboard = seat .add_keyboard(XkbConfig::default(), 200, 25, |seat, focus| { set_data_device_focus(seat, focus.and_then(|s| s.as_ref().client())) @@ -159,6 +169,7 @@ impl AnvilState { cursor_status, pointer_location: (0.0, 0.0), seat_name, + seat, start_time: std::time::Instant::now(), #[cfg(feature = "xwayland")] xwayland, diff --git a/src/backend/input.rs b/src/backend/input.rs index 91091ac..7def5a8 100644 --- a/src/backend/input.rs +++ b/src/backend/input.rs @@ -1,6 +1,13 @@ //! Common traits for input backends to receive input from. -use std::error::Error; +use std::{error::Error, path::PathBuf}; + +mod tablet; + +pub use tablet::{ + ProximityState, TabletToolAxisEvent, TabletToolButtonEvent, TabletToolCapabilitys, TabletToolDescriptor, + TabletToolEvent, TabletToolProximityEvent, TabletToolTipEvent, TabletToolTipState, TabletToolType, +}; /// Trait for generic functions every input device does provide pub trait Device: PartialEq + Eq + std::hash::Hash { @@ -12,6 +19,14 @@ pub trait Device: PartialEq + Eq + std::hash::Hash { fn name(&self) -> String; /// Test if this device has a specific capability fn has_capability(&self, capability: DeviceCapability) -> bool; + + /// Returns device USB (product,vendor) id + fn usb_id(&self) -> Option<(u32, u32)>; + + /// Returns the syspath of the device. + /// + /// The path is an absolute path and includes the sys mount point. + fn syspath(&self) -> Option; } /// Set of input types a device may provide @@ -105,9 +120,9 @@ pub enum MouseButton { Other(u8), } -/// State of a button on a mouse. Either pressed or released +/// State of a button on a pointer device, like mouse or tablet tool. Either pressed or released #[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum MouseButtonState { +pub enum ButtonState { /// Button is released Released, /// Button is pressed @@ -119,7 +134,7 @@ pub trait PointerButtonEvent: Event { /// Pressed button of the event fn button(&self) -> MouseButton; /// State of the button - fn state(&self) -> MouseButtonState; + fn state(&self) -> ButtonState; } impl PointerButtonEvent for UnusedEvent { @@ -127,7 +142,7 @@ impl PointerButtonEvent for UnusedEvent { match *self {} } - fn state(&self) -> MouseButtonState { + fn state(&self) -> ButtonState { match *self {} } } @@ -481,6 +496,14 @@ pub trait InputBackend: Sized { type TouchCancelEvent: TouchCancelEvent; /// Type representing touch frame events type TouchFrameEvent: TouchFrameEvent; + /// Type representing axis events on tablet devices + type TabletToolAxisEvent: TabletToolAxisEvent; + /// Type representing proximity events on tablet devices + type TabletToolProximityEvent: TabletToolProximityEvent; + /// Type representing tip events on tablet devices + type TabletToolTipEvent: TabletToolTipEvent; + /// Type representing button events on tablet tool devices + type TabletToolButtonEvent: TabletToolButtonEvent; /// Special events that are custom to this backend type SpecialEvent; @@ -557,6 +580,31 @@ pub enum InputEvent { /// The touch frame event event: B::TouchFrameEvent, }, + + /// A tablet tool axis was emited + TabletToolAxis { + /// The tablet tool axis event + event: B::TabletToolAxisEvent, + }, + + /// A tablet tool proximity was emited + TabletToolProximity { + /// The tablet tool proximity event + event: B::TabletToolProximityEvent, + }, + + /// A tablet tool tip event was emited + TabletToolTip { + /// The tablet tool axis event + event: B::TabletToolTipEvent, + }, + + /// A tablet tool button was pressed or released + TabletToolButton { + /// The pointer button event + event: B::TabletToolButtonEvent, + }, + /// Special event specific of this backend Special(B::SpecialEvent), } diff --git a/src/backend/input/tablet.rs b/src/backend/input/tablet.rs new file mode 100644 index 0000000..91813c7 --- /dev/null +++ b/src/backend/input/tablet.rs @@ -0,0 +1,366 @@ +use super::{ButtonState, Event, InputBackend, UnusedEvent}; +use bitflags::bitflags; + +/// Description of physical tablet tool +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub struct TabletToolDescriptor { + /// The tool type is the high-level type of the tool and usually decides the interaction expected from this tool. + pub tool_type: TabletToolType, + /// Unique hardware serial number of the tool + pub hardware_serial: u64, + /// Hardware id in Wacom’s format + pub hardware_id_wacom: u64, + /// Tool capability + /// Notifies the client of any capabilities of this tool, beyond the main set of x/y axes and tip up/down detection + pub capabilitys: TabletToolCapabilitys, +} + +/// Describes the physical type of a tool. The physical type of a tool generally defines its base usage. +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] +pub enum TabletToolType { + /// A generic pen. + Pen, + /// Eraser + Eraser, + /// A paintbrush-like tool. + Brush, + /// Physical drawing tool, e.g. Wacom Inking Pen + Pencil, + /// An airbrush-like tool. + Airbrush, + /// A mouse bound to the tablet. + Mouse, + /// A mouse tool with a lens. + Lens, + /// A rotary device with positional and rotation data. + Totem, +} + +bitflags! { + /// Describes extra capabilities on a tablet. + /// + /// Any tool must provide x and y values, extra axes are device-specific. + pub struct TabletToolCapabilitys: u32 { + /// Tilt axes + const TILT = 1; + /// Pressure axis + const PRESSURE = 2; + /// Distance axis + const DISTANCE = 4; + /// Z-rotation axis + const ROTATION = 16; + /// Slider axis + const SLIDER = 32; + /// Wheel axis + const WHEEL = 64; + } +} + +/// Tablet tool event +pub trait TabletToolEvent { + /// Get tablet tool that caused this event + fn tool(&self) -> TabletToolDescriptor; + + /// Delta between the last and new pointer device position interpreted as pixel movement + fn delta(&self) -> (f64, f64) { + (self.delta_x(), self.delta_y()) + } + + /// Tool position in the device's native coordinate space + fn position(&self) -> (f64, f64) { + (self.x(), self.y()) + } + + /// Tool position converted into the target coordinate space. + fn position_transformed(&self, coordinate_space: (u32, u32)) -> (f64, f64) { + ( + self.x_transformed(coordinate_space.0), + self.y_transformed(coordinate_space.1), + ) + } + + /// Returns the current tilt along the (X,Y) axis of the tablet's current logical + /// orientation, in degrees off the tablet's z axis. + /// + /// That is, if the tool is perfectly orthogonal to the tablet, the tilt angle is 0. + /// When the top tilts towards the logical top/left of the tablet, the x/y tilt + /// angles are negative, if the top tilts towards the logical bottom/right of the + /// tablet, the x/y tilt angles are positive. + /// + /// If this axis does not exist on the current tool, this function returns (0,0). + fn tilt(&self) -> (f64, f64) { + (self.tilt_x(), self.tilt_y()) + } + + /// Check if the tilt was updated in this event. + fn tilt_has_changed(&self) -> bool { + self.tilt_x_has_changed() || self.tilt_y_has_changed() + } + + /// Delta on the x axis between the last and new pointer device position interpreted as pixel movement + fn delta_x(&self) -> f64; + /// Delta on the y axis between the last and new pointer device position interpreted as pixel movement + fn delta_y(&self) -> f64; + + /// Returns the x coordinate of the tablet tool, in mm from the top left corner of the tablet in its current logical orientation. + fn x(&self) -> f64; + /// Returns the y coordinate of the tablet tool, in mm from the top left corner of the tablet in its current logical orientation. + fn y(&self) -> f64; + + /// Return the current absolute X coordinate of the tablet tool event, transformed to screen coordinates. + fn x_transformed(&self, width: u32) -> f64; + /// Return the current absolute Y coordinate of the tablet tool event, transformed to screen coordinates. + fn y_transformed(&self, height: u32) -> f64; + + /// Returns the current distance from the tablet's sensor, normalized to the range [0, 1] + /// + /// If this axis does not exist on the current tool, this function returns 0. + fn distance(&self) -> f64; + + /// Check if the distance axis was updated in this event. + fn distance_has_changed(&self) -> bool; + + /// Returns the current pressure being applied on the tool in use, normalized to the range [0, 1]. + /// + /// If this axis does not exist on the current tool, this function returns 0. + fn pressure(&self) -> f64; + + /// Check if the pressure axis was updated in this event. + fn pressure_has_changed(&self) -> bool; + + /// Returns the current position of the slider on the tool, normalized to the range + /// [-1, 1]. + /// + /// The logical zero is the neutral position of the slider, or the logical center of + /// the axis. This axis is available on e.g. the Wacom Airbrush. + /// + /// If this axis does not exist on the current tool, this function returns 0. + fn slider_position(&self) -> f64; + + /// Check if the slider axis was updated in this event. + fn slider_has_changed(&self) -> bool; + + /// Returns the current tilt along the X axis of the tablet's current logical + /// orientation, in degrees off the tablet's z axis. + /// + /// That is, if the tool is perfectly orthogonal to the tablet, the tilt angle is 0. + /// When the top tilts towards the logical top/left of the tablet, the x/y tilt + /// angles are negative, if the top tilts towards the logical bottom/right of the + /// tablet, the x/y tilt angles are positive. + /// + /// If this axis does not exist on the current tool, this function returns 0. + fn tilt_x(&self) -> f64; + + /// Check if the tilt x axis was updated in this event. + fn tilt_x_has_changed(&self) -> bool; + + /// Returns the current tilt along the Y axis of the tablet's current logical + /// orientation, in degrees off the tablet's z axis. + /// + /// That is, if the tool is perfectly orthogonal to the tablet, the tilt angle is 0. + /// When the top tilts towards the logical top/left of the tablet, the x/y tilt + /// angles are negative, if the top tilts towards the logical bottom/right of the + /// tablet, the x/y tilt angles are positive. + /// + /// If this axis does not exist on the current tool, this function returns 0. + fn tilt_y(&self) -> f64; + + /// Check if the tilt y axis was updated in this event. + fn tilt_y_has_changed(&self) -> bool; + + /// Returns the current z rotation of the tool in degrees, clockwise from the tool's logical neutral position. + /// + /// For tools of type Mouse and Lens the logical neutral position is pointing to the current logical north of the tablet. + /// For tools of type Brush, the logical neutral position is with the buttons pointing up. + /// + /// If this axis does not exist on the current tool, this function returns 0. + fn rotation(&self) -> f64; + + /// Check if the z-rotation axis was updated in this event. + fn rotation_has_changed(&self) -> bool; + + /// Return the delta for the wheel in degrees. + fn wheel_delta(&self) -> f64; + /// Return the delta for the wheel in discrete steps (e.g. wheel clicks). + fn wheel_delta_discrete(&self) -> i32; + /// Check if the wheel axis was updated in this event. + fn wheel_has_changed(&self) -> bool; +} + +impl TabletToolEvent for UnusedEvent { + fn tool(&self) -> TabletToolDescriptor { + match *self {} + } + fn delta_x(&self) -> f64 { + match *self {} + } + fn delta_y(&self) -> f64 { + match *self {} + } + fn x(&self) -> f64 { + match *self {} + } + fn y(&self) -> f64 { + match *self {} + } + fn x_transformed(&self, _width: u32) -> f64 { + match *self {} + } + fn y_transformed(&self, _height: u32) -> f64 { + match *self {} + } + fn distance(&self) -> f64 { + match *self {} + } + fn distance_has_changed(&self) -> bool { + match *self {} + } + fn pressure(&self) -> f64 { + match *self {} + } + fn pressure_has_changed(&self) -> bool { + match *self {} + } + fn slider_position(&self) -> f64 { + match *self {} + } + fn slider_has_changed(&self) -> bool { + match *self {} + } + fn tilt_x(&self) -> f64 { + match *self {} + } + fn tilt_x_has_changed(&self) -> bool { + match *self {} + } + fn tilt_y(&self) -> f64 { + match *self {} + } + fn tilt_y_has_changed(&self) -> bool { + match *self {} + } + fn rotation(&self) -> f64 { + match *self {} + } + fn rotation_has_changed(&self) -> bool { + match *self {} + } + fn wheel_delta(&self) -> f64 { + match *self {} + } + fn wheel_delta_discrete(&self) -> i32 { + match *self {} + } + fn wheel_has_changed(&self) -> bool { + match *self {} + } +} + +/// Trait for axis tablet tool events. +pub trait TabletToolAxisEvent: TabletToolEvent + Event {} + +impl TabletToolAxisEvent for UnusedEvent {} + +/// The state of proximity for a tool on a device. +/// +/// The proximity of a tool is a binary state signalling whether the tool is within a +/// detectable distance of the tablet device. A tool that is out of proximity cannot +/// generate events. +/// +/// On some hardware a tool goes out of proximity when it ceases to touch the surface. On +/// other hardware, the tool is still detectable within a short distance (a few cm) off +/// the surface. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ProximityState { + /// Out of proximity + Out, + /// In proximity + In, +} + +/// Trait for tablet tool proximity events. +pub trait TabletToolProximityEvent: TabletToolEvent + Event { + /// Returns the new proximity state of a tool from a proximity event. + /// + /// Used to check whether or not a tool came in or out of proximity during an + /// `TabletToolProximityEvent`. + /// + /// See [Handling of proximity events](https://wayland.freedesktop.org/libinput/doc/latest/tablet-support.html#tablet-fake-proximity) + /// for recommendations on proximity handling. + fn state(&self) -> ProximityState; +} + +impl TabletToolProximityEvent for UnusedEvent { + fn state(&self) -> ProximityState { + match *self {} + } +} + +/// The tip contact state for a tool on a device. +/// +/// The tip contact state of a tool is a binary state signalling whether the tool is +/// touching the surface of the tablet device. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum TabletToolTipState { + /// Not touching the surface + Up, + /// Touching the surface + Down, +} + +/// Signals that a tool has come in contact with the surface of a device with the +/// `DeviceCapability::TabletTool` capability. +/// +/// On devices without distance proximity detection, the `TabletToolTipEvent` is sent +/// immediately after `TabletToolProximityEvent` for the tip down event, and +/// immediately before for the tip up event. +/// +/// The decision when a tip touches the surface is device-dependent and may be +/// derived from pressure data or other means. If the tip state is changed by axes +/// changing state, the `TabletToolTipEvent` includes the changed axes and no +/// additional axis event is sent for this state change. In other words, a caller +/// must look at both `TabletToolAxisEvent` and `TabletToolTipEvent` events to know +/// the current state of the axes. +/// +/// If a button state change occurs at the same time as a tip state change, the order +/// of events is device-dependent. +pub trait TabletToolTipEvent: TabletToolEvent + Event { + /// Returns the new tip state of a tool from a tip event. + /// + /// Used to check whether or not a tool came in contact with the tablet surface or + /// left contact with the tablet surface during an `TabletToolTipEvent`. + fn tip_state(&self) -> TabletToolTipState; +} + +impl TabletToolTipEvent for UnusedEvent { + fn tip_state(&self) -> TabletToolTipState { + match *self {} + } +} + +/// Signals that a tool has changed a logical button state on a device with the DeviceCapability::TabletTool capability. +pub trait TabletToolButtonEvent: TabletToolEvent + Event { + /// Return the button that triggered this event. + fn button(&self) -> u32; + + /// For the button of a TabletToolButtonEvent, + /// return the total number of buttons pressed on all devices on the associated seat after the the event was triggered. + fn seat_button_count(&self) -> u32; + + /// Return the button state of the event. + fn button_state(&self) -> ButtonState; +} + +impl TabletToolButtonEvent for UnusedEvent { + fn button(&self) -> u32 { + match *self {} + } + + fn seat_button_count(&self) -> u32 { + match *self {} + } + + fn button_state(&self) -> ButtonState { + match *self {} + } +} diff --git a/src/backend/libinput.rs b/src/backend/libinput.rs index e4a5114..8da6a29 100644 --- a/src/backend/libinput.rs +++ b/src/backend/libinput.rs @@ -14,12 +14,15 @@ use std::path::Path; use std::{ io::Error as IoError, os::unix::io::{AsRawFd, RawFd}, + path::PathBuf, }; use calloop::{EventSource, Interest, Mode, Poll, PostAction, Readiness, Token, TokenFactory}; use slog::{info, o}; +mod tablet; + // No idea if this is the same across unix platforms // Lets make this linux exclusive for now, once someone tries to build it for // any BSD-like system, they can verify if this is right and make a PR to change this. @@ -93,6 +96,21 @@ impl backend::Device for libinput::Device { fn has_capability(&self, capability: backend::DeviceCapability) -> bool { libinput::Device::has_capability(self, capability.into()) } + + fn usb_id(&self) -> Option<(u32, u32)> { + Some(( + libinput::Device::id_product(self), + libinput::Device::id_vendor(self), + )) + } + + fn syspath(&self) -> Option { + #[cfg(feature = "udev")] + return unsafe { libinput::Device::udev_device(self) }.map(|d| d.syspath().to_owned()); + + #[cfg(not(feature = "udev"))] + None + } } impl From for libinput::DeviceCapability { @@ -179,7 +197,7 @@ impl backend::PointerButtonEvent for event::pointer::Point } } - fn state(&self) -> backend::MouseButtonState { + fn state(&self) -> backend::ButtonState { self.button_state().into() } } @@ -355,6 +373,10 @@ impl InputBackend for LibinputInputBackend { type TouchMotionEvent = event::touch::TouchMotionEvent; type TouchCancelEvent = event::touch::TouchCancelEvent; type TouchFrameEvent = event::touch::TouchFrameEvent; + type TabletToolAxisEvent = event::tablet_tool::TabletToolAxisEvent; + type TabletToolProximityEvent = event::tablet_tool::TabletToolProximityEvent; + type TabletToolTipEvent = event::tablet_tool::TabletToolTipEvent; + type TabletToolButtonEvent = event::tablet_tool::TabletToolButtonEvent; type SpecialEvent = backend::UnusedEvent; @@ -420,6 +442,20 @@ impl InputBackend for LibinputInputBackend { callback(InputEvent::PointerButton { event: button_event }); } }, + libinput::Event::Tablet(tablet_event) => match tablet_event { + event::TabletToolEvent::Axis(event) => { + callback(InputEvent::TabletToolAxis { event }); + } + event::TabletToolEvent::Proximity(event) => { + callback(InputEvent::TabletToolProximity { event }); + } + event::TabletToolEvent::Tip(event) => { + callback(InputEvent::TabletToolTip { event }); + } + event::TabletToolEvent::Button(event) => { + callback(InputEvent::TabletToolButton { event }); + } + }, _ => {} //FIXME: What to do with the rest. } } @@ -465,11 +501,11 @@ impl From for backend::AxisSource { } } -impl From for backend::MouseButtonState { +impl From for backend::ButtonState { fn from(libinput: event::pointer::ButtonState) -> Self { match libinput { - event::pointer::ButtonState::Pressed => backend::MouseButtonState::Pressed, - event::pointer::ButtonState::Released => backend::MouseButtonState::Released, + event::pointer::ButtonState::Pressed => backend::ButtonState::Pressed, + event::pointer::ButtonState::Released => backend::ButtonState::Released, } } } diff --git a/src/backend/libinput/tablet.rs b/src/backend/libinput/tablet.rs new file mode 100644 index 0000000..f32eed8 --- /dev/null +++ b/src/backend/libinput/tablet.rs @@ -0,0 +1,188 @@ +use crate::backend::input::{ + self as backend, TabletToolCapabilitys, TabletToolDescriptor, TabletToolTipState, TabletToolType, +}; + +use input as libinput; +use input::event; +use input::event::{tablet_tool, EventTrait}; + +use super::LibinputInputBackend; + +/// Marker for tablet tool events +pub trait IsTabletEvent: tablet_tool::TabletToolEventTrait + EventTrait {} + +impl IsTabletEvent for tablet_tool::TabletToolAxisEvent {} +impl IsTabletEvent for tablet_tool::TabletToolProximityEvent {} +impl IsTabletEvent for tablet_tool::TabletToolTipEvent {} +impl IsTabletEvent for tablet_tool::TabletToolButtonEvent {} + +impl backend::Event for E +where + E: IsTabletEvent, +{ + fn time(&self) -> u32 { + tablet_tool::TabletToolEventTrait::time(self) + } + + fn device(&self) -> libinput::Device { + event::EventTrait::device(self) + } +} + +impl backend::TabletToolAxisEvent for tablet_tool::TabletToolAxisEvent {} + +impl backend::TabletToolProximityEvent for tablet_tool::TabletToolProximityEvent { + fn state(&self) -> backend::ProximityState { + match tablet_tool::TabletToolProximityEvent::proximity_state(self) { + tablet_tool::ProximityState::In => backend::ProximityState::In, + tablet_tool::ProximityState::Out => backend::ProximityState::Out, + } + } +} + +impl backend::TabletToolTipEvent for tablet_tool::TabletToolTipEvent { + fn tip_state(&self) -> TabletToolTipState { + match tablet_tool::TabletToolTipEvent::tip_state(self) { + tablet_tool::TipState::Up => backend::TabletToolTipState::Up, + tablet_tool::TipState::Down => backend::TabletToolTipState::Down, + } + } +} + +impl backend::TabletToolEvent for E +where + E: IsTabletEvent + event::EventTrait, +{ + fn tool(&self) -> TabletToolDescriptor { + let tool = self.tool(); + + let tool_type = match tool.tool_type() { + tablet_tool::TabletToolType::Pen => TabletToolType::Pen, + tablet_tool::TabletToolType::Eraser => TabletToolType::Eraser, + tablet_tool::TabletToolType::Brush => TabletToolType::Brush, + tablet_tool::TabletToolType::Pencil => TabletToolType::Pencil, + tablet_tool::TabletToolType::Airbrush => TabletToolType::Airbrush, + tablet_tool::TabletToolType::Mouse => TabletToolType::Mouse, + tablet_tool::TabletToolType::Lens => TabletToolType::Lens, + tablet_tool::TabletToolType::Totem => TabletToolType::Totem, + }; + + let hardware_serial = tool.serial(); + let hardware_id_wacom = tool.tool_id(); + + let mut capabilitys = TabletToolCapabilitys::empty(); + + capabilitys.set(TabletToolCapabilitys::TILT, tool.has_tilt()); + capabilitys.set(TabletToolCapabilitys::PRESSURE, tool.has_pressure()); + capabilitys.set(TabletToolCapabilitys::DISTANCE, tool.has_distance()); + capabilitys.set(TabletToolCapabilitys::ROTATION, tool.has_rotation()); + capabilitys.set(TabletToolCapabilitys::SLIDER, tool.has_slider()); + capabilitys.set(TabletToolCapabilitys::WHEEL, tool.has_wheel()); + + TabletToolDescriptor { + tool_type, + hardware_serial, + hardware_id_wacom, + capabilitys, + } + } + + fn delta_x(&self) -> f64 { + tablet_tool::TabletToolEventTrait::dx(self) + } + + fn delta_y(&self) -> f64 { + tablet_tool::TabletToolEventTrait::dy(self) + } + + fn x(&self) -> f64 { + tablet_tool::TabletToolEventTrait::x(self) + } + + fn y(&self) -> f64 { + tablet_tool::TabletToolEventTrait::y(self) + } + + fn x_transformed(&self, width: u32) -> f64 { + tablet_tool::TabletToolEventTrait::x_transformed(self, width) + } + + fn y_transformed(&self, height: u32) -> f64 { + tablet_tool::TabletToolEventTrait::y_transformed(self, height) + } + + fn distance(&self) -> f64 { + tablet_tool::TabletToolEventTrait::distance(self) + } + + fn distance_has_changed(&self) -> bool { + tablet_tool::TabletToolEventTrait::distance_has_changed(self) + } + + fn pressure(&self) -> f64 { + tablet_tool::TabletToolEventTrait::pressure(self) + } + + fn pressure_has_changed(&self) -> bool { + tablet_tool::TabletToolEventTrait::pressure_has_changed(self) + } + + fn slider_position(&self) -> f64 { + tablet_tool::TabletToolEventTrait::slider_position(self) + } + + fn slider_has_changed(&self) -> bool { + tablet_tool::TabletToolEventTrait::slider_has_changed(self) + } + + fn tilt_x(&self) -> f64 { + tablet_tool::TabletToolEventTrait::dx(self) + } + + fn tilt_x_has_changed(&self) -> bool { + tablet_tool::TabletToolEventTrait::tilt_x_has_changed(self) + } + + fn tilt_y(&self) -> f64 { + tablet_tool::TabletToolEventTrait::dy(self) + } + + fn tilt_y_has_changed(&self) -> bool { + tablet_tool::TabletToolEventTrait::tilt_y_has_changed(self) + } + + fn rotation(&self) -> f64 { + tablet_tool::TabletToolEventTrait::rotation(self) + } + + fn rotation_has_changed(&self) -> bool { + tablet_tool::TabletToolEventTrait::rotation_has_changed(self) + } + + fn wheel_delta(&self) -> f64 { + tablet_tool::TabletToolEventTrait::wheel_delta(self) + } + + fn wheel_delta_discrete(&self) -> i32 { + // I have no idea why f64 is returend by this fn, in libinput's api wheel clicks are always i32 + tablet_tool::TabletToolEventTrait::wheel_delta_discrete(self) as i32 + } + + fn wheel_has_changed(&self) -> bool { + tablet_tool::TabletToolEventTrait::wheel_has_changed(self) + } +} + +impl backend::TabletToolButtonEvent for tablet_tool::TabletToolButtonEvent { + fn button(&self) -> u32 { + tablet_tool::TabletToolButtonEvent::button(&self) + } + + fn seat_button_count(&self) -> u32 { + tablet_tool::TabletToolButtonEvent::seat_button_count(&self) + } + + fn button_state(&self) -> backend::ButtonState { + tablet_tool::TabletToolButtonEvent::button_state(&self).into() + } +} diff --git a/src/backend/winit.rs b/src/backend/winit.rs index b560611..e62200a 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -23,8 +23,8 @@ use crate::backend::egl::display::EGLDisplay; use crate::backend::{ egl::{context::GlAttributes, native, EGLContext, EGLSurface, Error as EGLError}, input::{ - Axis, AxisSource, Device, DeviceCapability, Event as BackendEvent, InputBackend, InputEvent, - KeyState, KeyboardKeyEvent, MouseButton, MouseButtonState, PointerAxisEvent, PointerButtonEvent, + Axis, AxisSource, ButtonState, Device, DeviceCapability, Event as BackendEvent, InputBackend, + InputEvent, KeyState, KeyboardKeyEvent, MouseButton, PointerAxisEvent, PointerButtonEvent, PointerMotionAbsoluteEvent, TouchCancelEvent, TouchDownEvent, TouchMotionEvent, TouchSlot, TouchUpEvent, UnusedEvent, }, @@ -33,7 +33,7 @@ use crate::backend::{ Bind, Renderer, Transform, Unbind, }, }; -use std::{cell::RefCell, rc::Rc, time::Instant}; +use std::{cell::RefCell, path::PathBuf, rc::Rc, time::Instant}; use wayland_egl as wegl; use winit::{ dpi::{LogicalPosition, LogicalSize, PhysicalSize}, @@ -307,6 +307,14 @@ impl Device for WinitVirtualDevice { DeviceCapability::Keyboard | DeviceCapability::Pointer | DeviceCapability::Touch ) } + + fn usb_id(&self) -> Option<(u32, u32)> { + None + } + + fn syspath(&self) -> Option { + None + } } /// Errors that may happen when driving the event loop of [`WinitInputBackend`] @@ -460,7 +468,7 @@ impl PointerButtonEvent for WinitMouseInputEvent { self.button.into() } - fn state(&self) -> MouseButtonState { + fn state(&self) -> ButtonState { self.state.into() } } @@ -619,6 +627,10 @@ impl InputBackend for WinitInputBackend { type TouchMotionEvent = WinitTouchMovedEvent; type TouchCancelEvent = WinitTouchCancelledEvent; type TouchFrameEvent = UnusedEvent; + type TabletToolAxisEvent = UnusedEvent; + type TabletToolProximityEvent = UnusedEvent; + type TabletToolTipEvent = UnusedEvent; + type TabletToolButtonEvent = UnusedEvent; type SpecialEvent = WinitEvent; @@ -846,11 +858,11 @@ impl From for KeyState { } } -impl From for MouseButtonState { +impl From for ButtonState { fn from(state: ElementState) -> Self { match state { - ElementState::Pressed => MouseButtonState::Pressed, - ElementState::Released => MouseButtonState::Released, + ElementState::Pressed => ButtonState::Pressed, + ElementState::Released => ButtonState::Released, } } } diff --git a/src/wayland/mod.rs b/src/wayland/mod.rs index 3cd7b0e..37f2ab6 100644 --- a/src/wayland/mod.rs +++ b/src/wayland/mod.rs @@ -62,6 +62,7 @@ pub mod output; pub mod seat; pub mod shell; pub mod shm; +pub mod tablet_manager; /// A global [`SerialCounter`] for use in your compositor. /// diff --git a/src/wayland/tablet_manager/mod.rs b/src/wayland/tablet_manager/mod.rs new file mode 100644 index 0000000..b083859 --- /dev/null +++ b/src/wayland/tablet_manager/mod.rs @@ -0,0 +1,89 @@ +//! Utilities for graphics tablet support +//! +//! This module provides helpers to handle graphics tablets. +//! +//! ``` +//! # extern crate wayland_server; +//! # #[macro_use] extern crate smithay; +//! # use smithay::wayland::compositor::compositor_init; +//! +//! use smithay::wayland::seat::{Seat}; +//! use smithay::wayland::tablet_manager::{init_tablet_manager_global, TabletSeatTrait, TabletDescriptor}; +//! +//! # let mut display = wayland_server::Display::new(); +//! # compositor_init(&mut display, |_, _| {}, None); +//! // First we nee a reguler seat +//! let (seat, seat_global) = Seat::new( +//! &mut display, +//! "seat-0".into(), +//! None +//! ); +//! +//! // Insert the manager global into your event loop +//! init_tablet_manager_global(&mut display); +//! +//! seat +//! .tablet_seat() // Get TabletSeat asosiated with this seat +//! .add_tablet(&TabletDescriptor { // Add a new tablet to a seat +//! name: "Test".into(), +//! usb_id: None, +//! syspath: None, +//! }); +//! +//! ``` + +use crate::wayland::seat::Seat; +use wayland_protocols::unstable::tablet::v2::server::zwp_tablet_manager_v2::{self, ZwpTabletManagerV2}; +use wayland_server::{Display, Filter, Global, Main}; + +const MANAGER_VERSION: u32 = 1; + +mod tablet; +mod tablet_seat; +mod tablet_tool; + +pub use tablet::{TabletDescriptor, TabletHandle}; +pub use tablet_seat::TabletSeatHandle; +pub use tablet_tool::TabletToolHandle; + +/// Extends [Seat] with graphic tablet specyfic functionality +pub trait TabletSeatTrait { + /// Get tablet seat asosiated with this seat + fn tablet_seat(&self) -> TabletSeatHandle; +} + +impl TabletSeatTrait for Seat { + fn tablet_seat(&self) -> TabletSeatHandle { + let user_data = self.user_data(); + user_data.insert_if_missing(TabletSeatHandle::default); + user_data.get::().unwrap().clone() + } +} + +/// Initialize a tablet manager global. +pub fn init_tablet_manager_global(display: &mut Display) -> Global { + display.create_global::( + MANAGER_VERSION, + Filter::new( + move |(manager, _version): (Main, u32), _, _| { + manager.quick_assign(|_manager, req, _| match req { + zwp_tablet_manager_v2::Request::GetTabletSeat { tablet_seat, seat } => { + let seat = Seat::from_resource(&seat).unwrap(); + + let user_data = seat.user_data(); + user_data.insert_if_missing(TabletSeatHandle::default); + + let instance = tablet_seat; + let tablet_seat = user_data.get::().unwrap(); + + tablet_seat.add_instance(instance); + } + zwp_tablet_manager_v2::Request::Destroy => { + // Nothing to do + } + _ => {} + }); + }, + ), + ) +} diff --git a/src/wayland/tablet_manager/tablet.rs b/src/wayland/tablet_manager/tablet.rs new file mode 100644 index 0000000..bb1c8a2 --- /dev/null +++ b/src/wayland/tablet_manager/tablet.rs @@ -0,0 +1,95 @@ +use std::ops::Deref as _; +use std::path::PathBuf; +use std::{cell::RefCell, rc::Rc}; + +use wayland_protocols::unstable::tablet::v2::server::zwp_tablet_seat_v2::ZwpTabletSeatV2; +use wayland_protocols::unstable::tablet::v2::server::zwp_tablet_v2::ZwpTabletV2; +use wayland_server::protocol::wl_surface::WlSurface; +use wayland_server::Filter; + +use crate::backend::input::Device; + +/// Description of graphics tablet device +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub struct TabletDescriptor { + /// Tablet device name + pub name: String, + /// Tablet device USB (product,vendor) id + pub usb_id: Option<(u32, u32)>, + /// Path to the device + pub syspath: Option, +} + +impl From<&D> for TabletDescriptor { + fn from(device: &D) -> Self { + TabletDescriptor { + name: device.name(), + syspath: device.syspath(), + usb_id: device.usb_id(), + } + } +} + +#[derive(Debug, Default)] +struct Tablet { + instances: Vec, +} + +/// Handle to a tablet device +/// +/// Tablet represents one graphics tablet device +#[derive(Debug, Default, Clone)] +pub struct TabletHandle { + inner: Rc>, +} + +impl TabletHandle { + pub(super) fn new_instance(&mut self, seat: &ZwpTabletSeatV2, tablet: &TabletDescriptor) { + if let Some(client) = seat.as_ref().client() { + let wl_tablet = client + .create_resource::(seat.as_ref().version()) + .unwrap(); + + wl_tablet.quick_assign(|_, _req, _| {}); + + let inner = self.inner.clone(); + wl_tablet.assign_destructor(Filter::new(move |instance: ZwpTabletV2, _, _| { + inner + .borrow_mut() + .instances + .retain(|i| !i.as_ref().equals(&instance.as_ref())); + })); + + seat.tablet_added(&wl_tablet); + + wl_tablet.name(tablet.name.clone()); + + if let Some((id_product, id_vendor)) = tablet.usb_id { + wl_tablet.id(id_product, id_vendor); + } + + if let Some(syspath) = tablet.syspath.as_ref().and_then(|p| p.to_str()) { + wl_tablet.path(syspath.to_owned()); + } + + wl_tablet.done(); + + self.inner.borrow_mut().instances.push(wl_tablet.deref().clone()); + } + } + + pub(super) fn with_focused_tablet(&self, focus: &WlSurface, cb: F) + where + F: Fn(&ZwpTabletV2), + { + if let Some(instance) = self + .inner + .borrow() + .instances + .iter() + .find(|i| i.as_ref().same_client_as(focus.as_ref())) + { + cb(instance); + } + } +} diff --git a/src/wayland/tablet_manager/tablet_seat.rs b/src/wayland/tablet_manager/tablet_seat.rs new file mode 100644 index 0000000..ea3d489 --- /dev/null +++ b/src/wayland/tablet_manager/tablet_seat.rs @@ -0,0 +1,189 @@ +use wayland_protocols::unstable::tablet::v2::server::zwp_tablet_seat_v2::ZwpTabletSeatV2; +use wayland_server::{Filter, Main}; + +use crate::backend::input::TabletToolDescriptor; +use crate::wayland::seat::CursorImageStatus; + +use super::tablet::{TabletDescriptor, TabletHandle}; +use super::tablet_tool::TabletToolHandle; + +use std::convert::AsRef; +use std::fmt; +use std::ops::Deref as _; +use std::{cell::RefCell, collections::HashMap, rc::Rc}; + +#[derive(Default)] +struct TabletSeat { + instances: Vec, + tablets: HashMap, + tools: HashMap, + + cursor_callback: Option>, +} + +impl fmt::Debug for TabletSeat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TabletSeat") + .field("instances", &self.instances) + .field("tablets", &self.tablets) + .field("tools", &self.tools) + .field( + "cursor_callback", + if self.cursor_callback.is_some() { + &"Some(...)" + } else { + &"None" + }, + ) + .finish() + } +} + +/// Handle to a tablet seat +/// +/// TabletSeat extends `Seat` with graphic tablet specyfic functionality +/// +/// TabletSeatHandle can be used to advertise available graphics tablets and tools to wayland clients +#[derive(Default, Debug, Clone)] +pub struct TabletSeatHandle { + inner: Rc>, +} + +impl TabletSeatHandle { + pub(super) fn add_instance(&self, seat: Main) { + let mut inner = self.inner.borrow_mut(); + + // Notify new instance about available tablets + for (desc, tablet) in inner.tablets.iter_mut() { + tablet.new_instance(seat.deref(), desc); + } + + // Notify new instance about available tools + for (desc, tool) in inner.tools.iter_mut() { + let inner = self.inner.clone(); + tool.new_instance(seat.deref(), desc, move |desc, status| { + if let Some(ref mut cursor_callback) = inner.borrow_mut().cursor_callback { + cursor_callback(desc, status); + } + }); + } + + inner.instances.push(seat.deref().clone()); + + let inner = self.inner.clone(); + seat.assign_destructor(Filter::new(move |seat: ZwpTabletSeatV2, _, _| { + inner + .borrow_mut() + .instances + .retain(|i| !i.as_ref().equals(&seat.as_ref())); + })); + } + + /// Add a callback to SetCursor event + pub fn on_cursor_surface(&mut self, cb: F) + where + F: FnMut(&TabletToolDescriptor, CursorImageStatus) + 'static, + { + self.inner.borrow_mut().cursor_callback = Some(Box::new(cb)); + } + + /// Add a new tablet to a seat. + /// + /// You can either add tablet on [input::Event::DeviceAdded](crate::backend::input::InputEvent::DeviceAdded) event, + /// or you can add tablet based on tool event, then clients will not know about devices that are not being used + /// + /// Returns new [TabletHandle] if tablet was not know by this seat, if tablet was allready know it returns exsisting handle. + pub fn add_tablet(&self, tablet_desc: &TabletDescriptor) -> TabletHandle { + let inner = &mut *self.inner.borrow_mut(); + + let tablets = &mut inner.tablets; + let instances = &inner.instances; + + let tablet = tablets.entry(tablet_desc.clone()).or_insert_with(|| { + let mut tablet = TabletHandle::default(); + // Create new tablet instance for every seat instance + for seat in instances.iter() { + tablet.new_instance(seat, tablet_desc); + } + tablet + }); + + tablet.clone() + } + + /// Get a handle to a tablet + pub fn get_tablet(&self, tablet_desc: &TabletDescriptor) -> Option { + self.inner.borrow().tablets.get(tablet_desc).cloned() + } + + /// Count all tablet devices + pub fn count_tablets(&self) -> usize { + self.inner.borrow_mut().tablets.len() + } + + /// Remove tablet device + /// + /// Called when tablet is no longer avalible + /// For example on [input::Event::DeviceRemoved](crate::backend::input::InputEvent::DeviceRemoved) event. + pub fn remove_tablet(&self, tablet_desc: &TabletDescriptor) { + self.inner.borrow_mut().tablets.remove(tablet_desc); + } + + /// Remove all tablet devices + pub fn clear_tablets(&self) { + self.inner.borrow_mut().tablets.clear(); + } + + /// Add a new tool to a seat. + /// + /// Tool is usually added on [TabletToolProximityEvent](crate::backend::input::InputEvent::TabletToolProximity) event. + /// + /// Returns new [TabletToolHandle] if tool was not know by this seat, if tool was allready know it returns exsisting handle, + /// it allows you to send tool input events to clients. + pub fn add_tool(&self, tool_desc: &TabletToolDescriptor) -> TabletToolHandle { + let inner = &mut *self.inner.borrow_mut(); + + let tools = &mut inner.tools; + let instances = &inner.instances; + + let tool = tools.entry(tool_desc.clone()).or_insert_with(|| { + let mut tool = TabletToolHandle::default(); + // Create new tool instance for every seat instance + for seat in instances.iter() { + let inner = self.inner.clone(); + tool.new_instance(seat.deref(), tool_desc, move |desc, status| { + if let Some(ref mut cursor_callback) = inner.borrow_mut().cursor_callback { + cursor_callback(desc, status); + } + }); + } + tool + }); + + tool.clone() + } + + /// Get a handle to a tablet tool + pub fn get_tool(&self, tool_desc: &TabletToolDescriptor) -> Option { + self.inner.borrow().tools.get(tool_desc).cloned() + } + + /// Count all tablet tool devices + pub fn count_tools(&self) -> usize { + self.inner.borrow_mut().tools.len() + } + + /// Remove tablet tool device + /// + /// Policy of tool removal is a compositor-specific. + /// + /// One posible policy would be to remove a tool when all tablets the tool was used on are removed. + pub fn remove_tool(&self, tool_desc: &TabletToolDescriptor) { + self.inner.borrow_mut().tools.remove(tool_desc); + } + + /// Remove all tablet tool devices + pub fn clear_tools(&self) { + self.inner.borrow_mut().tools.clear(); + } +} diff --git a/src/wayland/tablet_manager/tablet_tool.rs b/src/wayland/tablet_manager/tablet_tool.rs new file mode 100644 index 0000000..19cb770 --- /dev/null +++ b/src/wayland/tablet_manager/tablet_tool.rs @@ -0,0 +1,477 @@ +use std::ops::Deref as _; +use std::sync::Mutex; +use std::{cell::RefCell, rc::Rc}; + +use crate::backend::input::{ButtonState, TabletToolCapabilitys, TabletToolDescriptor, TabletToolType}; +use crate::wayland::seat::{CursorImageAttributes, CursorImageStatus}; +use wayland_protocols::unstable::tablet::v2::server::{ + zwp_tablet_seat_v2::ZwpTabletSeatV2, + zwp_tablet_tool_v2::{self, ZwpTabletToolV2}, +}; +use wayland_server::protocol::wl_surface::WlSurface; +use wayland_server::Filter; + +use crate::wayland::{compositor, Serial}; + +use super::tablet::TabletHandle; + +static CURSOR_IMAGE_ROLE: &str = "cursor_image"; + +#[derive(Debug, Default)] +struct TabletTool { + instances: Vec, + focus: Option, + + is_down: bool, + + pending_pressure: Option, + pending_distance: Option, + pending_tilt: Option<(f64, f64)>, + pending_slider: Option, + pending_rotation: Option, + pending_wheel: Option<(f64, i32)>, +} + +impl TabletTool { + fn proximity_in( + &mut self, + (x, y): (f64, f64), + (focus, (sx, sy)): (WlSurface, (f64, f64)), + tablet: &TabletHandle, + serial: Serial, + time: u32, + ) { + let wl_tool = self + .instances + .iter() + .find(|i| i.as_ref().same_client_as(focus.as_ref())); + + if let Some(wl_tool) = wl_tool { + tablet.with_focused_tablet(&focus, |wl_tablet| { + wl_tool.proximity_in(serial.into(), wl_tablet, &focus); + // proximity_in has to be followed by motion event (required by protocol) + wl_tool.motion(x - sx, y - sy); + wl_tool.frame(time); + }); + } + + self.focus = Some(focus.clone()); + } + + fn proximity_out(&mut self, time: u32) { + if let Some(ref focus) = self.focus { + let wl_tool = self + .instances + .iter() + .find(|i| i.as_ref().same_client_as(focus.as_ref())); + + if let Some(wl_tool) = wl_tool { + if self.is_down { + wl_tool.up(); + self.is_down = false; + } + wl_tool.proximity_out(); + wl_tool.frame(time); + } + } + + self.focus = None; + } + + fn tip_down(&mut self, serial: Serial, time: u32) { + if let Some(ref focus) = self.focus { + if let Some(wl_tool) = self + .instances + .iter() + .find(|i| i.as_ref().same_client_as(focus.as_ref())) + { + if !self.is_down { + wl_tool.down(serial.into()); + wl_tool.frame(time); + } + } + } + + self.is_down = true; + } + + fn tip_up(&mut self, time: u32) { + if let Some(ref focus) = self.focus { + if let Some(wl_tool) = self + .instances + .iter() + .find(|i| i.as_ref().same_client_as(focus.as_ref())) + { + if self.is_down { + wl_tool.up(); + wl_tool.frame(time); + } + } + } + + self.is_down = false; + } + + fn motion( + &mut self, + pos: (f64, f64), + focus: Option<(WlSurface, (f64, f64))>, + tablet: &TabletHandle, + serial: Serial, + time: u32, + ) { + match (focus, self.focus.as_ref()) { + (Some(focus), Some(prev_focus)) => { + if &focus.0 == prev_focus { + if let Some(wl_tool) = self + .instances + .iter() + .find(|i| i.as_ref().same_client_as(focus.0.as_ref())) + { + let (x, y) = pos; + let (sx, sy) = focus.1; + wl_tool.motion(x - sx, y - sy); + + if let Some(pressure) = self.pending_pressure.take() { + wl_tool.pressure((pressure * 65535.0).round() as u32); + } + + if let Some(distance) = self.pending_distance.take() { + wl_tool.distance((distance * 65535.0).round() as u32); + } + + if let Some((x, y)) = self.pending_tilt.take() { + wl_tool.tilt(x, y); + } + + if let Some(slider) = self.pending_slider.take() { + wl_tool.slider((slider * 65535.0).round() as i32); + } + + if let Some(rotation) = self.pending_rotation.take() { + wl_tool.rotation(rotation); + } + + if let Some((degrees, clicks)) = self.pending_wheel.take() { + wl_tool.wheel(degrees, clicks) + } + + wl_tool.frame(time); + } + } else { + // If surface has changed + + // Unfocus previous surface + self.proximity_out(time); + // Focuss a new one + self.proximity_in(pos, focus, tablet, serial, time) + } + } + // New surface in focus + (Some(focus), None) => self.proximity_in(pos, focus, tablet, serial, time), + // No surface in focus + (None, _) => self.proximity_out(time), + } + } + + fn pressure(&mut self, pressure: f64) { + self.pending_pressure = Some(pressure); + } + + fn distance(&mut self, distance: f64) { + self.pending_distance = Some(distance); + } + + fn tilt(&mut self, tilt: (f64, f64)) { + self.pending_tilt = Some(tilt); + } + + fn rotation(&mut self, rotation: f64) { + self.pending_rotation = Some(rotation); + } + + fn slider_position(&mut self, slider: f64) { + self.pending_slider = Some(slider); + } + + fn wheel(&mut self, degrees: f64, clicks: i32) { + self.pending_wheel = Some((degrees, clicks)); + } + + /// Sent whenever a button on the tool is pressed or released. + fn button(&self, button: u32, state: ButtonState, serial: Serial, time: u32) { + if let Some(ref focus) = self.focus { + if let Some(wl_tool) = self + .instances + .iter() + .find(|i| i.as_ref().same_client_as(focus.as_ref())) + { + wl_tool.button(serial.into(), button, state.into()); + wl_tool.frame(time); + } + } + } +} + +impl Drop for TabletTool { + fn drop(&mut self) { + for instance in self.instances.iter() { + // This event is sent when the tool is removed from the system and will send no further events. + instance.removed(); + } + } +} + +/// Handle to a tablet tool device +/// +/// TabletTool represents a physical tool that has been, or is currently in use with a tablet in seat. +/// +/// A TabletTool relation to a physical tool depends on the tablet's ability to report serial numbers. If the tablet supports this capability, then the object represents a specific physical tool and can be identified even when used on multiple tablets. +#[derive(Debug, Default, Clone)] +pub struct TabletToolHandle { + inner: Rc>, +} + +impl TabletToolHandle { + pub(super) fn new_instance(&mut self, seat: &ZwpTabletSeatV2, tool: &TabletToolDescriptor, mut cb: F) + where + F: FnMut(&TabletToolDescriptor, CursorImageStatus) + 'static, + { + if let Some(client) = seat.as_ref().client() { + let wl_tool = client + .create_resource::(seat.as_ref().version()) + .unwrap(); + + let desc = tool.clone(); + let inner = self.inner.clone(); + wl_tool.quick_assign(move |tool, req, _| { + use wayland_protocols::unstable::tablet::v2::server::zwp_tablet_tool_v2::Request; + match req { + Request::SetCursor { + surface, + hotspot_x, + hotspot_y, + .. + } => { + let inner = inner.borrow(); + + if let Some(ref focus) = inner.focus { + if focus.as_ref().same_client_as(&tool.as_ref()) { + if let Some(surface) = surface { + // tolerate re-using the same surface + if compositor::give_role(&surface, CURSOR_IMAGE_ROLE).is_err() + && compositor::get_role(&surface) != Some(CURSOR_IMAGE_ROLE) + { + tool.as_ref().post_error( + zwp_tablet_tool_v2::Error::Role as u32, + "Given wl_surface has another role.".into(), + ); + return; + } + + compositor::with_states(&surface, |states| { + states.data_map.insert_if_missing_threadsafe(|| { + Mutex::new(CursorImageAttributes { hotspot: (0, 0) }) + }); + states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap() + .hotspot = (hotspot_x, hotspot_y); + }) + .unwrap(); + + cb(&desc, CursorImageStatus::Image(surface)); + } else { + cb(&desc, CursorImageStatus::Hidden); + }; + } + } + } + Request::Destroy => { + // Handled by our destructor + } + _ => {} + } + }); + + let inner = self.inner.clone(); + wl_tool.assign_destructor(Filter::new(move |instance: ZwpTabletToolV2, _, _| { + inner + .borrow_mut() + .instances + .retain(|i| !i.as_ref().equals(&instance.as_ref())); + })); + + seat.tool_added(&wl_tool); + + wl_tool._type(tool.tool_type.into()); + + let high: u32 = (tool.hardware_serial >> 16) as u32; + let low: u32 = tool.hardware_serial as u32; + + wl_tool.hardware_serial(high, low); + + let high: u32 = (tool.hardware_id_wacom >> 16) as u32; + let low: u32 = tool.hardware_id_wacom as u32; + wl_tool.hardware_id_wacom(high, low); + + if tool.capabilitys.contains(TabletToolCapabilitys::PRESSURE) { + wl_tool.capability(zwp_tablet_tool_v2::Capability::Pressure); + } + + if tool.capabilitys.contains(TabletToolCapabilitys::DISTANCE) { + wl_tool.capability(zwp_tablet_tool_v2::Capability::Distance); + } + + if tool.capabilitys.contains(TabletToolCapabilitys::TILT) { + wl_tool.capability(zwp_tablet_tool_v2::Capability::Tilt); + } + + if tool.capabilitys.contains(TabletToolCapabilitys::SLIDER) { + wl_tool.capability(zwp_tablet_tool_v2::Capability::Slider); + } + + if tool.capabilitys.contains(TabletToolCapabilitys::ROTATION) { + wl_tool.capability(zwp_tablet_tool_v2::Capability::Rotation); + } + + if tool.capabilitys.contains(TabletToolCapabilitys::WHEEL) { + wl_tool.capability(zwp_tablet_tool_v2::Capability::Wheel); + } + + wl_tool.done(); + self.inner.borrow_mut().instances.push(wl_tool.deref().clone()); + } + } + + /// Notifify that this tool is focused on a certain surface. + /// + /// You provide the location of the tool, in the form of: + /// + /// - The coordinates of the tool in the global compositor space + /// - The surface on top of which the tool is, and the coordinates of its + /// origin in the global compositor space. + pub fn proximity_in( + &self, + pos: (f64, f64), + focus: (WlSurface, (f64, f64)), + tablet: &TabletHandle, + serial: Serial, + time: u32, + ) { + self.inner + .borrow_mut() + .proximity_in(pos, focus, tablet, serial, time) + } + + /// Notifify that this tool has left proximity. + pub fn proximity_out(&self, time: u32) { + self.inner.borrow_mut().proximity_out(time); + } + + /// Tablet tool is making contact + pub fn tip_down(&self, serial: Serial, time: u32) { + self.inner.borrow_mut().tip_down(serial, time); + } + + /// Tablet tool is no longer making contact + pub fn tip_up(&self, time: u32) { + self.inner.borrow_mut().tip_up(time); + } + + /// Notify that the tool moved + /// + /// You provide the new location of the tool, in the form of: + /// + /// - The coordinates of the tool in the global compositor space + /// - The surface on top of which the tool is, and the coordinates of its + /// origin in the global compositor space (or `None` of the pointer is not + /// on top of a client surface). + /// + /// This will internally take care of notifying the appropriate client objects + /// of proximity_in/proximity_out events. + pub fn motion( + &self, + pos: (f64, f64), + focus: Option<(WlSurface, (f64, f64))>, + tablet: &TabletHandle, + serial: Serial, + time: u32, + ) { + self.inner.borrow_mut().motion(pos, focus, tablet, serial, time) + } + + /// Queue tool pressure update + /// + /// It will be sent alongside next motion event + pub fn pressure(&self, pressure: f64) { + self.inner.borrow_mut().pressure(pressure); + } + + /// Queue tool distance update + /// + /// It will be sent alongside next motion event + pub fn distance(&self, distance: f64) { + self.inner.borrow_mut().distance(distance); + } + + /// Queue tool tilt update + /// + /// It will be sent alongside next motion event + pub fn tilt(&self, tilt: (f64, f64)) { + self.inner.borrow_mut().tilt(tilt); + } + + /// Queue tool rotation update + /// + /// It will be sent alongside next motion event + pub fn rotation(&self, rotation: f64) { + self.inner.borrow_mut().rotation(rotation); + } + + /// Queue tool slider update + /// + /// It will be sent alongside next motion event + pub fn slider_position(&self, slider: f64) { + self.inner.borrow_mut().slider_position(slider); + } + + /// Queue tool wheel update + /// + /// It will be sent alongside next motion event + pub fn wheel(&self, degrees: f64, clicks: i32) { + self.inner.borrow_mut().wheel(degrees, clicks); + } + + /// Button on the tool was pressed or released + pub fn button(&self, button: u32, state: ButtonState, serial: Serial, time: u32) { + self.inner.borrow().button(button, state, serial, time); + } +} + +impl From for zwp_tablet_tool_v2::Type { + fn from(from: TabletToolType) -> zwp_tablet_tool_v2::Type { + match from { + TabletToolType::Pen => zwp_tablet_tool_v2::Type::Pen, + TabletToolType::Eraser => zwp_tablet_tool_v2::Type::Eraser, + TabletToolType::Brush => zwp_tablet_tool_v2::Type::Brush, + TabletToolType::Pencil => zwp_tablet_tool_v2::Type::Pencil, + TabletToolType::Airbrush => zwp_tablet_tool_v2::Type::Airbrush, + TabletToolType::Mouse => zwp_tablet_tool_v2::Type::Mouse, + TabletToolType::Lens => zwp_tablet_tool_v2::Type::Lens, + _ => zwp_tablet_tool_v2::Type::Pen, + } + } +} + +impl From for zwp_tablet_tool_v2::ButtonState { + fn from(from: ButtonState) -> zwp_tablet_tool_v2::ButtonState { + match from { + ButtonState::Pressed => zwp_tablet_tool_v2::ButtonState::Pressed, + ButtonState::Released => zwp_tablet_tool_v2::ButtonState::Released, + } + } +}