Skip to content

Support tablet-v2 #489

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 27 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
94a60f8
Support most of tablet-v2
chris-morgan Mar 16, 2025
eb83d74
[WIP] Expand the tablet example for pseudo-drawing
chris-morgan Mar 18, 2025
5d11d0b
One approach to collecting tablet metadata
chris-morgan Mar 19, 2025
8ae623e
Tablet info events: new approach, always collect
chris-morgan Mar 19, 2025
0c8f542
Naming: “description”
chris-morgan Mar 19, 2025
1ced013
Restructure code somewhat
chris-morgan Mar 19, 2025
61e0e44
Try a more radical module and naming structure
chris-morgan Mar 19, 2025
d6741e1
Fiddle with tablet_tool::InitEvent naming
chris-morgan Mar 19, 2025
a649d55
Accumulate description for tablet tool handlers
chris-morgan Mar 19, 2025
29b63c4
Use bitflags for tool capabilities
chris-morgan Mar 19, 2025
efdd0b4
Quite a bit of poorly-separated work
chris-morgan Mar 26, 2025
0004673
Remove dead code
chris-morgan Mar 26, 2025
666c52c
Revise some tablet naming
chris-morgan Mar 26, 2025
b7cb645
Update some docs, remove obsolete bits
chris-morgan Mar 26, 2025
568a552
Refine how Tools is exposed
chris-morgan Mar 26, 2025
98f3aad
Make tablet seat handler methods optional
chris-morgan Mar 26, 2025
2b8df7d
(Advice sought) Remove seat from tablet seat handler methods
chris-morgan Mar 26, 2025
387d718
Document that no other axis methods are warranted
chris-morgan Mar 28, 2025
5cd8a95
Remove more dead code
chris-morgan Mar 29, 2025
53eabb9
More shuffling tool stuff around, for the better
chris-morgan Mar 29, 2025
b19c603
Add cursors to the tablet tool example
chris-morgan Mar 29, 2025
ae0667f
Make TabletManager largely implicit, via SeatState
chris-morgan Mar 29, 2025
d13e2ec
Refactor manager globals, expose cursor shape manager
chris-morgan Mar 29, 2025
9e4c056
Tidy up example a bit, improve logging
chris-morgan Mar 29, 2025
c9fe2f6
Support cursor shapes on tablet tools, a bit
chris-morgan Mar 29, 2025
7d7f20d
Refactor SeatState::new for subjective prettiness
chris-morgan Mar 29, 2025
5dd2be3
Make {tablet, tablet_tool}::Info non-exhaustive
chris-morgan Mar 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
736 changes: 736 additions & 0 deletions examples/tablet.rs

Large diffs are not rendered by default.

146 changes: 98 additions & 48 deletions src/seat/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand All @@ -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 {
Expand Down Expand Up @@ -65,48 +76,80 @@ pub enum SeatError {
pub struct SeatState {
// (name, seat)
seats: Vec<SeatInner>,
cursor_shape_manager_state: CursorShapeManagerState,
cursor_shape_manager_state: ManagerState<CursorShapeManager>,
tablet_manager_state: ManagerState<TabletManager>,
}

#[derive(Debug)]
enum CursorShapeManagerState {
NotPresent,
enum ManagerState<T> {
Failed(BindError),
Pending { registry: WlRegistry, global: Global },
Bound(CursorShapeManager),
Bound(T),
}

impl<T> ManagerState<T> {
fn bind<I, D>(&mut self, qh: &QueueHandle<D>, version: std::ops::RangeInclusive<u32>, f: impl FnOnce(I) -> T) -> Result<&T, BindError>
where
I: Proxy + 'static,
D: Dispatch<I, GlobalData> + 'static,
{
*self = match std::mem::replace(self, ManagerState::Failed(BindError::NotPresent)) {
ManagerState::Pending { registry, global } => {
match crate::registry::bind_one(&registry, &[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 {
pub fn new<D: Dispatch<wl_seat::WlSeat, SeatData> + 'static>(
global_list: &GlobalList,
qh: &QueueHandle<D>,
) -> 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::<SeatData>().unwrap().clone();
Expand Down Expand Up @@ -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))),
Expand All @@ -276,6 +302,16 @@ impl SeatState {
})
}

pub fn cursor_shape_manager<D>(&mut self, qh: &QueueHandle<D>)
-> Result<&CursorShapeManager, BindError>
where
D: Dispatch<WpCursorShapeManagerV1, GlobalData>,
D: Dispatch<WpCursorShapeDeviceV1, GlobalData>,
D: 'static,
{
self.cursor_shape_manager_state.bind(qh, 1..=1, CursorShapeManager::from_existing)
}

/// Creates a touch handle from a seat.
///
/// ## Errors
Expand Down Expand Up @@ -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<D>(
&mut self,
qh: &QueueHandle<D>,
seat: &wl_seat::WlSeat,
) -> Result<ZwpTabletSeatV2, BindError>
where
D: Dispatch<ZwpTabletSeatV2, ()> + 'static,
D: Dispatch<ZwpTabletManagerV2, GlobalData> + '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 {
Expand Down
12 changes: 12 additions & 0 deletions src/seat/pointer/cursor_shape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -40,6 +41,17 @@ impl CursorShapeManager {
self.cursor_shape_manager.get_pointer(pointer, queue_handle, GlobalData)
}

pub fn get_shape_device_for_tablet_tool<State>(
&self,
tablet_tool: &ZwpTabletToolV2,
queue_handle: &QueueHandle<State>,
) -> WpCursorShapeDeviceV1
where
State: Dispatch<WpCursorShapeDeviceV1, GlobalData> + 'static,
{
self.cursor_shape_manager.get_tablet_tool_v2(tablet_tool, queue_handle, GlobalData)
}

pub fn inner(&self) -> &WpCursorShapeManagerV1 {
&self.cursor_shape_manager
}
Expand Down
90 changes: 90 additions & 0 deletions src/seat/tablet.rs
Original file line number Diff line number Diff line change
@@ -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<Self>,
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?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be a good time to destroy the object, but I suppose it isn't strictly responsible for it.

fn removed(
&mut self,
conn: &Connection,
qh: &QueueHandle<Self>,
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<String>,
/// 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<String>,
}

#[doc(hidden)]
#[derive(Debug)]
pub struct Data {
info: Mutex<Info>,
}

impl Data {
pub fn new() -> Self {
Self { info: Default::default() }
}
}

impl<D> Dispatch<ZwpTabletV2, Data, D>
for super::TabletManager
where
D: Dispatch<ZwpTabletV2, Data> + Handler,
{
fn event(
data: &mut D,
tablet: &ZwpTabletV2,
event: zwp_tablet_v2::Event,
udata: &Data,
conn: &Connection,
qh: &QueueHandle<D>,
) {
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!(),
}
}
}
Loading