From c77b66cc937ec6ab9143c6ce1beb86c70e284296 Mon Sep 17 00:00:00 2001 From: Shawn Wallace Date: Thu, 17 Oct 2024 01:33:56 -0400 Subject: [PATCH] Add tablet support Closes #47 --- src/clientside.rs | 169 +++++++++++++++++++++++++++- src/server/dispatch.rs | 229 ++++++++++++++++++++++++++++++++++---- src/server/event.rs | 236 +++++++++++++++++++++++++++++++++++++++ src/server/mod.rs | 31 ++++-- src/server/tests.rs | 244 ++++++++++++++++++++++++++++++++++++----- testwl/src/lib.rs | 82 ++++++++++++++ 6 files changed, 925 insertions(+), 66 deletions(-) diff --git a/src/clientside.rs b/src/clientside.rs index 956a97a..fd1df3c 100644 --- a/src/clientside.rs +++ b/src/clientside.rs @@ -1,5 +1,6 @@ use crate::server::{ObjectEvent, ObjectKey}; use std::os::unix::net::UnixStream; +use std::sync::{mpsc, Mutex, Once, OnceLock}; use wayland_client::protocol::{ wl_buffer::WlBuffer, wl_callback::WlCallback, wl_compositor::WlCompositor, wl_keyboard::WlKeyboard, wl_output::WlOutput, wl_pointer::WlPointer, wl_region::WlRegion, @@ -7,7 +8,7 @@ use wayland_client::protocol::{ wl_surface::WlSurface, wl_touch::WlTouch, }; use wayland_client::{ - delegate_noop, + delegate_noop, event_created_child, globals::{registry_queue_init, Global, GlobalList, GlobalListContents}, Connection, Dispatch, EventQueue, Proxy, QueueHandle, }; @@ -27,6 +28,19 @@ use wayland_protocols::{ zwp_locked_pointer_v1::ZwpLockedPointerV1, zwp_pointer_constraints_v1::ZwpPointerConstraintsV1, }, + tablet::zv2::client::{ + zwp_tablet_manager_v2::ZwpTabletManagerV2, + zwp_tablet_pad_group_v2::{ZwpTabletPadGroupV2, EVT_RING_OPCODE, EVT_STRIP_OPCODE}, + zwp_tablet_pad_ring_v2::ZwpTabletPadRingV2, + zwp_tablet_pad_strip_v2::ZwpTabletPadStripV2, + zwp_tablet_pad_v2::{ZwpTabletPadV2, EVT_GROUP_OPCODE}, + zwp_tablet_seat_v2::{ + ZwpTabletSeatV2, EVT_PAD_ADDED_OPCODE, EVT_TABLET_ADDED_OPCODE, + EVT_TOOL_ADDED_OPCODE, + }, + zwp_tablet_tool_v2::ZwpTabletToolV2, + zwp_tablet_v2::ZwpTabletV2, + }, viewporter::client::{wp_viewport::WpViewport, wp_viewporter::WpViewporter}, }, xdg::{ @@ -44,7 +58,8 @@ use wl_drm::client::wl_drm::WlDrm; #[derive(Default)] pub struct Globals { - pub(crate) events: Vec<(ObjectKey, ObjectEvent)>, + events: Vec<(ObjectKey, ObjectEvent)>, + queued_events: Vec>, pub new_globals: Vec, pub selection: Option, pub selection_requests: Vec<( @@ -57,7 +72,7 @@ pub struct Globals { pub type ClientQueueHandle = QueueHandle; pub struct ClientState { - pub connection: Connection, + _connection: Connection, pub queue: EventQueue, pub qh: ClientQueueHandle, pub globals: Globals, @@ -77,13 +92,31 @@ impl ClientState { let qh = queue.handle(); Self { - connection, + _connection: connection, queue, qh, globals, global_list, } } + + pub fn read_events(&mut self) -> Vec<(ObjectKey, ObjectEvent)> { + let mut events = std::mem::take(&mut self.globals.events); + self.globals.queued_events.retain(|rx| { + match rx.try_recv() { + Ok(event) => { + events.push(event); + } + Err(std::sync::mpsc::TryRecvError::Empty) => return true, + + Err(_) => unreachable!(), + } + + events.extend(rx.try_iter()); + false + }); + events + } } pub type Event = ::Event; @@ -100,6 +133,7 @@ delegate_noop!(Globals: WpViewporter); delegate_noop!(Globals: WpViewport); delegate_noop!(Globals: ZxdgOutputManagerV1); delegate_noop!(Globals: ZwpPointerConstraintsV1); +delegate_noop!(Globals: ZwpTabletManagerV2); impl Dispatch for Globals { fn event( @@ -188,3 +222,130 @@ push_events!(XdgOutput); push_events!(WlTouch); push_events!(ZwpConfinedPointerV1); push_events!(ZwpLockedPointerV1); + +pub(crate) struct LateInitObjectKey { + key: OnceLock, + queued_events: Mutex>, + sender: Mutex>>, +} + +impl LateInitObjectKey

+where + P::Event: Into, +{ + pub fn init(&self, key: ObjectKey) { + self.key.set(key).expect("Object key should not be set"); + if let Some(sender) = self.sender.lock().unwrap().take() { + for event in self.queued_events.lock().unwrap().drain(..) { + sender.send((key, event.into())).unwrap(); + } + } + } + + fn new() -> Self { + Self { + key: OnceLock::new(), + queued_events: Mutex::default(), + sender: Mutex::default(), + } + } + + fn push_or_queue_event(&self, state: &mut Globals, event: P::Event) { + if let Some(key) = self.key.get().copied() { + state.events.push((key, event.into())); + } else { + let mut sender = self.sender.lock().unwrap(); + if sender.is_none() { + let (send, recv) = mpsc::channel(); + *sender = Some(send); + state.queued_events.push(recv); + } + self.queued_events.lock().unwrap().push(event); + } + } +} + +impl std::ops::Deref for LateInitObjectKey

{ + type Target = ObjectKey; + + #[track_caller] + fn deref(&self) -> &Self::Target { + self.key.get().expect("object key has not been initialized") + } +} + +impl Dispatch for Globals { + fn event( + state: &mut Self, + _: &ZwpTabletSeatV2, + event: ::Event, + key: &ObjectKey, + _: &Connection, + _: &QueueHandle, + ) { + state.events.push((*key, event.into())); + } + + event_created_child!(Globals, ZwpTabletSeatV2, [ + EVT_TABLET_ADDED_OPCODE => (ZwpTabletV2, LateInitObjectKey::new()), + EVT_PAD_ADDED_OPCODE => (ZwpTabletPadV2, LateInitObjectKey::new()), + EVT_TOOL_ADDED_OPCODE => (ZwpTabletToolV2, LateInitObjectKey::new()) + ]); +} + +macro_rules! push_or_queue_events { + ($type:ty) => { + impl Dispatch<$type, LateInitObjectKey<$type>> for Globals { + fn event( + state: &mut Self, + _: &$type, + event: <$type as Proxy>::Event, + key: &LateInitObjectKey<$type>, + _: &Connection, + _: &QueueHandle, + ) { + key.push_or_queue_event(state, event); + } + } + }; +} + +push_or_queue_events!(ZwpTabletV2); +push_or_queue_events!(ZwpTabletToolV2); +push_or_queue_events!(ZwpTabletPadRingV2); +push_or_queue_events!(ZwpTabletPadStripV2); + +impl Dispatch> for Globals { + fn event( + state: &mut Self, + _: &ZwpTabletPadV2, + event: ::Event, + key: &LateInitObjectKey, + _: &Connection, + _: &QueueHandle, + ) { + key.push_or_queue_event(state, event); + } + + event_created_child!(Globals, ZwpTabletPadV2, [ + EVT_GROUP_OPCODE => (ZwpTabletPadGroupV2, LateInitObjectKey::new()) + ]); +} + +impl Dispatch> for Globals { + fn event( + state: &mut Self, + _: &ZwpTabletPadGroupV2, + event: ::Event, + key: &LateInitObjectKey, + _: &Connection, + _: &QueueHandle, + ) { + key.push_or_queue_event(state, event); + } + + event_created_child!(Globals, ZwpTabletPadGroupV2, [ + EVT_RING_OPCODE => (ZwpTabletPadRingV2, LateInitObjectKey::new()), + EVT_STRIP_OPCODE => (ZwpTabletPadStripV2, LateInitObjectKey::new()) + ]); +} diff --git a/src/server/dispatch.rs b/src/server/dispatch.rs index a2cc7e1..59febaf 100644 --- a/src/server/dispatch.rs +++ b/src/server/dispatch.rs @@ -20,11 +20,9 @@ use wayland_protocols::{ }, relative_pointer::zv1::{ client::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1 as RelativePointerManClient, - server::{ - zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1 as RelativePointerManServer, - zwp_relative_pointer_v1::ZwpRelativePointerV1 as RelativePointerServer, - }, + server::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1 as RelativePointerManServer, }, + tablet::zv2::{client as c_tablet, server as s_tablet}, viewporter::{client as c_vp, server as s_vp}, }, xdg::xdg_output::zv1::{ @@ -59,6 +57,40 @@ use wayland_server::{ Dispatch, DisplayHandle, GlobalDispatch, Resource, }; +macro_rules! only_destroy_request_impl { + ($object_type:ty) => { + impl Dispatch<<$object_type as GenericObjectExt>::Server, ObjectKey> + for ServerState + { + fn request( + state: &mut Self, + _: &Client, + _: &<$object_type as GenericObjectExt>::Server, + request: <<$object_type as GenericObjectExt>::Server as Resource>::Request, + key: &ObjectKey, + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + if !matches!( + request, + <<$object_type as GenericObjectExt>::Server as Resource>::Request::Destroy + ) { + warn!( + "unrecognized {} request: {:?}", + stringify!($object_type), + request + ); + return; + } + + let obj: &$object_type = state.objects[*key].as_ref(); + obj.client.destroy(); + state.objects.remove(*key); + } + } + }; +} + // noop impl Dispatch for ServerState { fn request( @@ -166,7 +198,7 @@ impl Dispatch for ServerS _: &DisplayHandle, _: &mut wayland_server::DataInit<'_, Self>, ) { - simple_event_shunt! { + macros::simple_event_shunt! { client, request: wl_region::Request => [ Add { x, y, width, height }, Subtract { x, y, width, height }, @@ -432,24 +464,7 @@ impl Dispatch for ServerState { } } } - -impl Dispatch for ServerState { - fn request( - state: &mut Self, - _: &wayland_server::Client, - _: &RelativePointerServer, - request: ::Request, - key: &ObjectKey, - _: &DisplayHandle, - _: &mut wayland_server::DataInit<'_, Self>, - ) { - if let Request::::Destroy = request { - let obj: &RelativePointer = state.objects[*key].as_ref(); - obj.client.destroy(); - state.objects.remove(*key); - } - } -} +only_destroy_request_impl!(RelativePointer); impl Dispatch> @@ -980,6 +995,170 @@ impl } } +impl + Dispatch< + s_tablet::zwp_tablet_manager_v2::ZwpTabletManagerV2, + ClientGlobalWrapper, + > for ServerState +{ + fn request( + state: &mut Self, + _: &wayland_server::Client, + _: &s_tablet::zwp_tablet_manager_v2::ZwpTabletManagerV2, + request: ::Request, + client: &ClientGlobalWrapper, + _: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + use s_tablet::zwp_tablet_manager_v2::Request::*; + match request { + GetTabletSeat { tablet_seat, seat } => { + let seat_key: ObjectKey = seat.data().copied().unwrap(); + state + .objects + .insert_from_other_objects([seat_key], |[seat_obj], key| { + let Seat { client: c_seat, .. }: &Seat = seat_obj.try_into().unwrap(); + let client = client.get_tablet_seat(c_seat, &state.qh, key); + let server = data_init.init(tablet_seat, key); + TabletSeat { client, server }.into() + }); + } + other => { + warn!("unhandled tablet request: {other:?}"); + } + } + } +} + +only_destroy_request_impl!(TabletSeat); +only_destroy_request_impl!(Tablet); +only_destroy_request_impl!(TabletPadGroup); + +impl Dispatch + for ServerState +{ + fn request( + state: &mut Self, + _: &Client, + _: &s_tablet::zwp_tablet_pad_v2::ZwpTabletPadV2, + request: ::Request, + key: &ObjectKey, + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + let pad: &TabletPad = state.objects[*key].as_ref(); + match request { + s_tablet::zwp_tablet_pad_v2::Request::SetFeedback { + button, + description, + serial, + } => { + pad.client.set_feedback(button, description, serial); + } + s_tablet::zwp_tablet_pad_v2::Request::Destroy => { + pad.client.destroy(); + state.objects.remove(*key); + } + other => warn!("unhandled tablet pad request: {other:?}"), + } + } +} + +impl Dispatch + for ServerState +{ + fn request( + state: &mut Self, + _: &Client, + _: &s_tablet::zwp_tablet_tool_v2::ZwpTabletToolV2, + request: ::Request, + key: &ObjectKey, + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + let tool: &TabletTool = state.objects[*key].as_ref(); + match request { + s_tablet::zwp_tablet_tool_v2::Request::SetCursor { + serial, + surface, + hotspot_x, + hotspot_y, + } => { + let surf_key: Option = surface.map(|s| s.data().copied().unwrap()); + let c_surface = surf_key.map(|key| { + let d: &SurfaceData = state.objects[key].as_ref(); + &d.client + }); + tool.client + .set_cursor(serial, c_surface, hotspot_x, hotspot_y); + } + s_tablet::zwp_tablet_tool_v2::Request::Destroy => { + tool.client.destroy(); + state.objects.remove(*key); + } + other => warn!("unhandled tablet tool request: {other:?}"), + } + } +} + +impl Dispatch + for ServerState +{ + fn request( + state: &mut Self, + _: &Client, + _: &s_tablet::zwp_tablet_pad_ring_v2::ZwpTabletPadRingV2, + request: ::Request, + key: &ObjectKey, + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + let ring: &TabletPadRing = state.objects[*key].as_ref(); + match request { + s_tablet::zwp_tablet_pad_ring_v2::Request::SetFeedback { + description, + serial, + } => { + ring.client.set_feedback(description, serial); + } + s_tablet::zwp_tablet_pad_ring_v2::Request::Destroy => { + ring.client.destroy(); + state.objects.remove(*key); + } + other => warn!("unhandled tablet pad ring requst: {other:?}"), + } + } +} + +impl Dispatch + for ServerState +{ + fn request( + state: &mut Self, + _: &Client, + _: &s_tablet::zwp_tablet_pad_strip_v2::ZwpTabletPadStripV2, + request: ::Request, + key: &ObjectKey, + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + let strip: &TabletPadStrip = state.objects[*key].as_ref(); + match request { + s_tablet::zwp_tablet_pad_strip_v2::Request::SetFeedback { + description, + serial, + } => { + strip.client.set_feedback(description, serial); + } + s_tablet::zwp_tablet_pad_strip_v2::Request::Destroy => { + strip.client.destroy(); + state.objects.remove(*key); + } + other => warn!("unhandled tablet pad strip requst: {other:?}"), + } + } +} + #[derive(Clone)] pub(crate) struct ClientGlobalWrapper(Arc>); impl std::ops::Deref for ClientGlobalWrapper { @@ -1072,6 +1251,10 @@ global_dispatch_no_events!( c_vp::wp_viewporter::WpViewporter ); global_dispatch_no_events!(PointerConstraintsServer, PointerConstraintsClient); +global_dispatch_no_events!( + s_tablet::zwp_tablet_manager_v2::ZwpTabletManagerV2, + c_tablet::zwp_tablet_manager_v2::ZwpTabletManagerV2 +); impl GlobalDispatch for ServerState where diff --git a/src/server/event.rs b/src/server/event.rs index c064441..2635d3a 100644 --- a/src/server/event.rs +++ b/src/server/event.rs @@ -1,4 +1,5 @@ use super::*; +use crate::clientside::LateInitObjectKey; use log::{debug, trace, warn}; use macros::simple_event_shunt; use std::collections::HashSet; @@ -22,6 +23,26 @@ use wayland_protocols::{ }, server::zwp_relative_pointer_v1::ZwpRelativePointerV1 as RelativePointerServer, }, + tablet::zv2::{ + client::{ + zwp_tablet_pad_group_v2::{self, ZwpTabletPadGroupV2 as TabletPadGroupClient}, + zwp_tablet_pad_ring_v2::{self, ZwpTabletPadRingV2 as TabletPadRingClient}, + zwp_tablet_pad_strip_v2::{self, ZwpTabletPadStripV2 as TabletPadStripClient}, + zwp_tablet_pad_v2::{self, ZwpTabletPadV2 as TabletPadClient}, + zwp_tablet_seat_v2::{self, ZwpTabletSeatV2 as TabletSeatClient}, + zwp_tablet_tool_v2::{self, ZwpTabletToolV2 as TabletToolClient}, + zwp_tablet_v2::{self, ZwpTabletV2 as TabletClient}, + }, + server::{ + zwp_tablet_pad_group_v2::ZwpTabletPadGroupV2 as TabletPadGroupServer, + zwp_tablet_pad_ring_v2::ZwpTabletPadRingV2 as TabletPadRingServer, + zwp_tablet_pad_strip_v2::ZwpTabletPadStripV2 as TabletPadStripServer, + zwp_tablet_pad_v2::ZwpTabletPadV2 as TabletPadServer, + zwp_tablet_seat_v2::ZwpTabletSeatV2 as TabletSeatServer, + zwp_tablet_tool_v2::ZwpTabletToolV2 as TabletToolServer, + zwp_tablet_v2::ZwpTabletV2 as TabletServer, + }, + }, }, xdg::{ shell::client::{xdg_popup, xdg_surface, xdg_toplevel}, @@ -237,6 +258,41 @@ pub struct GenericObject { pub client: Client, } +impl GenericObject { + fn from_client(client: C, state: &mut ServerState) -> &Self + where + Self: Into, + for<'a> &'a Self: TryFrom<&'a Object, Error = String>, + ServerState: wayland_server::Dispatch, + C::Event: Send + Into, + { + let key = state.objects.insert_with_key(|key| { + let server = state + .client + .as_ref() + .unwrap() + .create_resource::<_, _, ServerState>(&state.dh, 1, key) + .unwrap(); + let obj_key: &LateInitObjectKey = client.data().unwrap(); + obj_key.init(key); + + Self { client, server }.into() + }); + + state.objects[key].as_ref() + } +} + +pub trait GenericObjectExt { + type Server: Resource; + type Client: Proxy; +} + +impl GenericObjectExt for GenericObject { + type Server = S; + type Client = C; +} + pub type Buffer = GenericObject; impl HandleEvent for Buffer { type Event = client::wl_buffer::Event; @@ -790,3 +846,183 @@ impl HandleEvent for ConfinedPointer { } } } + +pub type TabletSeat = GenericObject; +impl HandleEvent for TabletSeat { + type Event = zwp_tablet_seat_v2::Event; + + fn handle_event(&mut self, event: Self::Event, state: &mut ServerState) { + simple_event_shunt! { + self.server, event: zwp_tablet_seat_v2::Event => [ + TabletAdded { + |id| &Tablet::from_client(id, state).server + }, + ToolAdded { + |id| &TabletTool::from_client(id, state).server + }, + PadAdded { + |id| &TabletPad::from_client(id, state).server + } + ] + } + } +} + +pub type Tablet = GenericObject; +impl HandleEvent for Tablet { + type Event = zwp_tablet_v2::Event; + + fn handle_event(&mut self, event: Self::Event, _: &mut ServerState) { + simple_event_shunt! { + self.server, event: zwp_tablet_v2::Event => [ + Name { name }, + Id { vid, pid }, + Path { path }, + Done, + Removed + ] + } + } +} + +pub type TabletPad = GenericObject; +impl HandleEvent for TabletPad { + type Event = zwp_tablet_pad_v2::Event; + + fn handle_event(&mut self, event: Self::Event, state: &mut ServerState) { + simple_event_shunt! { + self.server, event: zwp_tablet_pad_v2::Event => [ + Group { |pad_group| &TabletPadGroup::from_client(pad_group, state).server }, + Path { path }, + Buttons { buttons }, + Done, + Button { + time, + button, + |state| convert_wenum(state) + }, + Enter { + serial, + |tablet| { + let key: &LateInitObjectKey = tablet.data().unwrap(); + let Some(tablet): Option<&Tablet> = state.objects.get(**key).map(|o| o.as_ref()) else { + return; + }; + &tablet.server + }, + |surface| { + let Some(surface_data) = state.get_server_surface_from_client(surface) else { + return; + }; + surface_data + } + }, + Leave { + serial, + |surface| { + let Some(surface_data) = state.get_server_surface_from_client(surface) else { + return; + }; + surface_data + } + }, + Removed + ] + } + } +} + +pub type TabletTool = GenericObject; +impl HandleEvent for TabletTool { + type Event = zwp_tablet_tool_v2::Event; + + fn handle_event(&mut self, event: Self::Event, state: &mut ServerState) { + simple_event_shunt! { + self.server, event: zwp_tablet_tool_v2::Event => [ + Type { |tool_type| convert_wenum(tool_type) }, + HardwareSerial { hardware_serial_hi, hardware_serial_lo }, + HardwareIdWacom { hardware_id_hi, hardware_id_lo }, + Capability { |capability| convert_wenum(capability) }, + Done, + Removed, + ProximityIn { + serial, + |tablet| { + let key: &LateInitObjectKey = tablet.data().unwrap(); + let Some(tablet): Option<&Tablet> = state.objects.get(**key).map(|o| o.as_ref()) else { + return; + }; + &tablet.server + }, + |surface| { + let Some(surface_data) = state.get_server_surface_from_client(surface) else { + return; + }; + surface_data + } + }, + ProximityOut, + Down { serial }, + Up, + Motion { x, y }, + Pressure { pressure }, + Tilt { tilt_x, tilt_y }, + Rotation { degrees }, + Slider { position }, + Wheel { degrees, clicks }, + Button { serial, button, |state| convert_wenum(state) }, + Frame { time }, + ] + } + } +} + +pub type TabletPadGroup = GenericObject; +impl HandleEvent for TabletPadGroup { + type Event = zwp_tablet_pad_group_v2::Event; + + fn handle_event(&mut self, event: Self::Event, state: &mut ServerState) { + simple_event_shunt! { + self.server, event: zwp_tablet_pad_group_v2::Event => [ + Buttons { buttons }, + Ring { |ring| &TabletPadRing::from_client(ring, state).server }, + Strip { |strip| &TabletPadStrip::from_client(strip, state).server }, + Modes { modes }, + Done, + ModeSwitch { time, serial, mode } + ] + } + } +} + +pub type TabletPadRing = GenericObject; +impl HandleEvent for TabletPadRing { + type Event = zwp_tablet_pad_ring_v2::Event; + + fn handle_event(&mut self, event: Self::Event, _: &mut ServerState) { + simple_event_shunt! { + self.server, event: zwp_tablet_pad_ring_v2::Event => [ + Source { |source| convert_wenum(source) }, + Angle { degrees }, + Stop, + Frame { time } + ] + } + } +} + +pub type TabletPadStrip = GenericObject; +impl HandleEvent for TabletPadStrip { + type Event = zwp_tablet_pad_strip_v2::Event; + + fn handle_event(&mut self, event: Self::Event, _: &mut ServerState) { + simple_event_shunt! { + self.server, event: zwp_tablet_pad_strip_v2::Event => [ + Source { |source| convert_wenum(source) }, + Position { position }, + Stop, + Frame { time } + ] + } + } +} diff --git a/src/server/mod.rs b/src/server/mod.rs index f0922b2..f23cd95 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -28,6 +28,7 @@ use wayland_protocols::{ linux_dmabuf::zv1::{client as c_dmabuf, server as s_dmabuf}, pointer_constraints::zv1::server::zwp_pointer_constraints_v1::ZwpPointerConstraintsV1, relative_pointer::zv1::server::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1, + tablet::zv2::server::zwp_tablet_manager_v2::ZwpTabletManagerV2, viewporter::server as s_vp, }, xdg::{ @@ -49,7 +50,7 @@ use wayland_server::{ wl_callback::WlCallback, wl_compositor::WlCompositor, wl_output::WlOutput, wl_seat::WlSeat, wl_shm::WlShm, wl_surface::WlSurface, }, - DisplayHandle, Resource, WEnum, + Client, DisplayHandle, Resource, WEnum, }; use wl_drm::{client::wl_drm::WlDrm as WlDrmClient, server::wl_drm::WlDrm as WlDrmServer}; use xcb::x; @@ -362,12 +363,20 @@ pub(crate) enum Object { Drm(Drm), Touch(Touch), ConfinedPointer(ConfinedPointer), - LockedPointer(LockedPointer) + LockedPointer(LockedPointer), + TabletSeat(TabletSeat), + Tablet(Tablet), + TabletTool(TabletTool), + TabletPad(TabletPad), + TabletPadGroup(TabletPadGroup), + TabletPadRing(TabletPadRing), + TabletPadStrip(TabletPadStrip) } } -struct WrappedObject(Option); +#[derive(Default)] +pub(crate) struct WrappedObject(Option); impl From for WrappedObject where @@ -450,7 +459,8 @@ fn handle_globals<'a, C: XConnection>( s_dmabuf::zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, ZxdgOutputManagerV1, s_vp::wp_viewporter::WpViewporter, - ZwpPointerConstraintsV1 + ZwpPointerConstraintsV1, + ZwpTabletManagerV2 ]; } } @@ -467,6 +477,7 @@ pub struct ServerState { windows: HashMap, qh: ClientQueueHandle, + client: Option, to_focus: Option, last_focused_toplevel: Option, last_hovered: Option, @@ -511,6 +522,7 @@ impl ServerState { Self { windows: HashMap::new(), clientside, + client: None, atoms: None, qh, dh, @@ -531,9 +543,11 @@ impl ServerState { } pub fn connect(&mut self, connection: UnixStream) { - self.dh - .insert_client(connection, std::sync::Arc::new(())) - .unwrap(); + self.client = Some( + self.dh + .insert_client(connection, std::sync::Arc::new(())) + .unwrap(), + ); } pub fn set_x_connection(&mut self, connection: C) { @@ -800,8 +814,7 @@ impl ServerState { pub fn handle_clientside_events(&mut self) { self.handle_new_globals(); - let client_events = std::mem::take(&mut self.clientside.globals.events); - for (key, event) in client_events { + for (key, event) in self.clientside.read_events() { let Some(object) = &mut self.objects.get_mut(key) else { warn!("could not handle clientside event: stale surface"); continue; diff --git a/src/server/tests.rs b/src/server/tests.rs index 3f44b63..2c34362 100644 --- a/src/server/tests.rs +++ b/src/server/tests.rs @@ -23,11 +23,27 @@ use wayland_client::{ }, Connection, Proxy, WEnum, }; + use wayland_protocols::{ wp::{ linux_dmabuf::zv1::client::zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, pointer_constraints::zv1::client::zwp_pointer_constraints_v1::ZwpPointerConstraintsV1, relative_pointer::zv1::client::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1, + tablet::zv2::client::{ + zwp_tablet_manager_v2::{self, ZwpTabletManagerV2}, + zwp_tablet_pad_group_v2::{ + self, ZwpTabletPadGroupV2, EVT_RING_OPCODE, EVT_STRIP_OPCODE, + }, + zwp_tablet_pad_ring_v2::ZwpTabletPadRingV2, + zwp_tablet_pad_strip_v2::ZwpTabletPadStripV2, + zwp_tablet_pad_v2::{self, ZwpTabletPadV2, EVT_GROUP_OPCODE}, + zwp_tablet_seat_v2::{ + self, ZwpTabletSeatV2, EVT_PAD_ADDED_OPCODE, EVT_TABLET_ADDED_OPCODE, + EVT_TOOL_ADDED_OPCODE, + }, + zwp_tablet_tool_v2::{self, ZwpTabletToolV2}, + zwp_tablet_v2::{self, ZwpTabletV2}, + }, viewporter::client::wp_viewporter::WpViewporter, }, xdg::{ @@ -93,6 +109,7 @@ struct Compositor { shm: TestObject, shell: TestObject, seat: TestObject, + tablet_man: TestObject } } @@ -298,6 +315,23 @@ impl TestFixture { let events = std::mem::take(&mut *self.registry.data.events.lock().unwrap()); assert!(!events.is_empty()); + fn bind( + registry: &TestObject, + name: u32, + version: u32, + ) -> TestObject + where + T::Event: Sync + Send + std::fmt::Debug, + { + TestObject::from_request( + ®istry.obj, + Req::::Bind { + name, + id: (T::interface(), version), + }, + ) + } + for event in events { if let Ev::::Global { name, @@ -305,36 +339,18 @@ impl TestFixture { version, } = event { - let bind_req = |interface| Req::::Bind { - name, - id: (interface, version), - }; + macro_rules! bind { + ($field:ident) => { + ret.$field = Some(bind(&self.registry, name, version)) + }; + } match interface { - x if x == WlCompositor::interface().name => { - ret.compositor = Some(TestObject::from_request( - &self.registry.obj, - bind_req(WlCompositor::interface()), - )); - } - x if x == WlShm::interface().name => { - ret.shm = Some(TestObject::from_request( - &self.registry.obj, - bind_req(WlShm::interface()), - )); - } - x if x == XwaylandShellV1::interface().name => { - ret.shell = Some(TestObject::from_request( - &self.registry.obj, - bind_req(XwaylandShellV1::interface()), - )); - } - x if x == WlSeat::interface().name => { - ret.seat = Some(TestObject::from_request( - &self.registry.obj, - bind_req(WlSeat::interface()), - )); - } + x if x == WlCompositor::interface().name => bind!(compositor), + x if x == WlShm::interface().name => bind!(shm), + x if x == XwaylandShellV1::interface().name => bind!(shell), + x if x == WlSeat::interface().name => bind!(seat), + x if x == ZwpTabletManagerV2::interface().name => bind!(tablet_man), _ => {} } } @@ -349,6 +365,18 @@ impl TestFixture { ret.into() } + fn object_data

(&self, obj: &P) -> Arc> + where + P: Proxy + Send + Sync + 'static, + P::Event: Send + Sync + std::fmt::Debug, + { + self.xwls_connection + .get_object_data(obj.id()) + .unwrap() + .downcast_arc::>() + .unwrap() + } + /// Cascade our requests/events through satellite and testwl fn run(&mut self) { // Flush our requests to satellite @@ -709,10 +737,35 @@ where backend: &Backend, msg: Message, ) -> Option> { + fn obj_data() -> Arc + where + T: Send + Sync + 'static, + T::Event: Send + Sync + std::fmt::Debug, + { + Arc::new(TestObjectData::::default()) + } + + let new_data = match (msg.sender_id.interface().name, msg.opcode) { + (x, opcode) if x == ZwpTabletSeatV2::interface().name => match opcode { + EVT_TABLET_ADDED_OPCODE => Some(obj_data::()), + EVT_TOOL_ADDED_OPCODE => Some(obj_data::()), + EVT_PAD_ADDED_OPCODE => Some(obj_data::()), + _ => None, + }, + (x, EVT_GROUP_OPCODE) if x == ZwpTabletPadV2::interface().name => { + Some(obj_data::()) + } + (x, opcode) if x == ZwpTabletPadGroupV2::interface().name => match opcode { + EVT_RING_OPCODE => Some(obj_data::()), + EVT_STRIP_OPCODE => Some(obj_data::()), + _ => None, + }, + _ => None, + }; let connection = Connection::from_backend(backend.clone()); let event = T::parse_event(&connection, msg).unwrap().1; self.events.lock().unwrap().push(event); - None + new_data } fn destroyed(&self, _: ObjectId) {} @@ -849,7 +902,8 @@ fn pass_through_globals() { WpViewporter, WlDrm, ZwpPointerConstraintsV1, - XwaylandShellV1 + XwaylandShellV1, + ZwpTabletManagerV2 } let mut globals = SupportedGlobals::default(); @@ -1457,6 +1511,136 @@ fn ignore_toplevel_reconfigure() { ); } +#[track_caller] +fn events_check<'a, Event: std::fmt::Debug, const N: usize>( + mut it: impl Iterator, + mut matchers: [Box bool + 'a>; N], +) { + for (idx, matcher) in matchers.iter_mut().enumerate() { + let item = it.next(); + if item.is_none() { + panic!("event {idx} does not exist"); + } + if !matcher(item.as_ref().unwrap()) { + panic!("event {idx} was wrong ({item:?})"); + } + } + + let mut remaining = it.peekable(); + if remaining.peek().is_some() { + panic!("remaining events: {:?}", remaining.collect::>()); + } +} + +#[test] +fn tablet_smoke_test() { + let (mut f, comp) = TestFixture::new_with_compositor(); + let seat = TestObject::::from_request( + &comp.tablet_man.obj, + zwp_tablet_manager_v2::Request::GetTabletSeat { + seat: comp.seat.obj, + }, + ); + // Not sure why exactly this requires 4 runs but it works so idk + for _ in 0..4 { + f.run(); + } + + let events = std::mem::take(&mut *seat.data.events.lock().unwrap()).into_iter(); + let (mut tab_id, mut tool_id, mut pad_id) = (None, None, None); + events_check( + events, + [ + Box::new(|e| match e { + zwp_tablet_seat_v2::Event::TabletAdded { id } => { + tab_id = Some(id.clone()); + true + } + _ => false, + }), + Box::new(|e| match e { + zwp_tablet_seat_v2::Event::ToolAdded { id } => { + tool_id = Some(id.clone()); + true + } + _ => false, + }), + Box::new(|e| match e { + zwp_tablet_seat_v2::Event::PadAdded { id } => { + pad_id = Some(id.clone()); + true + } + _ => false, + }), + ], + ); + let (tab_id, tool_id, pad_id) = (tab_id.unwrap(), tool_id.unwrap(), pad_id.unwrap()); + + // For reasons beyond my mortal understanding, `id.object_data()` does not work properly. + let tab_data = f.object_data(&tab_id); + let tab_events = std::mem::take(&mut *tab_data.events.lock().unwrap()).into_iter(); + events_check( + tab_events, + [ + Box::new(|e| match e { + zwp_tablet_v2::Event::Name { name } if name == "tabby" => true, + _ => false, + }), + Box::new(|e| matches!(e, zwp_tablet_v2::Event::Done)), + ], + ); + + let tool_data = f.object_data(&tool_id); + let tool_events = std::mem::take(&mut *tool_data.events.lock().unwrap()).into_iter(); + events_check( + tool_events, + [ + Box::new(|e| { + matches!( + e, + zwp_tablet_tool_v2::Event::Type { + tool_type: WEnum::Value(zwp_tablet_tool_v2::Type::Finger) + } + ) + }), + Box::new(|e| matches!(e, zwp_tablet_tool_v2::Event::Done)), + ], + ); + + let pad_data = f.object_data(&pad_id); + let pad_events = std::mem::take(&mut *pad_data.events.lock().unwrap()).into_iter(); + let mut group = None; + events_check( + pad_events, + [ + Box::new(|e| matches!(e, zwp_tablet_pad_v2::Event::Buttons { buttons: 5 })), + Box::new(|e| match e { + zwp_tablet_pad_v2::Event::Group { pad_group } => { + group = Some(pad_group.clone()); + true + } + _ => false, + }), + Box::new(|e| matches!(e, zwp_tablet_pad_v2::Event::Done)), + ], + ); + + let group = group.unwrap(); + let g_data = f.object_data(&group); + let g_events = std::mem::take(&mut *g_data.events.lock().unwrap()).into_iter(); + events_check( + g_events, + [ + Box::new(|e| match e { + zwp_tablet_pad_group_v2::Event::Buttons { buttons } if buttons.is_empty() => true, + _ => false, + }), + Box::new(|e| matches!(e, zwp_tablet_pad_group_v2::Event::Ring { .. })), + Box::new(|e| matches!(e, zwp_tablet_pad_group_v2::Event::Strip { .. })), + Box::new(|e| matches!(e, zwp_tablet_pad_group_v2::Event::Done)), + ], + ) +} /// See Pointer::handle_event for an explanation. #[test] fn popup_pointer_motion_workaround() {} diff --git a/testwl/src/lib.rs b/testwl/src/lib.rs index 151b975..abe1b65 100644 --- a/testwl/src/lib.rs +++ b/testwl/src/lib.rs @@ -10,6 +10,16 @@ use wayland_protocols::{ linux_dmabuf::zv1::server::zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, pointer_constraints::zv1::server::zwp_pointer_constraints_v1::ZwpPointerConstraintsV1, relative_pointer::zv1::server::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1, + tablet::zv2::server::{ + zwp_tablet_manager_v2::ZwpTabletManagerV2, + zwp_tablet_pad_group_v2::ZwpTabletPadGroupV2, + zwp_tablet_pad_ring_v2::ZwpTabletPadRingV2, + zwp_tablet_pad_strip_v2::ZwpTabletPadStripV2, + zwp_tablet_pad_v2::ZwpTabletPadV2, + zwp_tablet_seat_v2::ZwpTabletSeatV2, + zwp_tablet_tool_v2::{self, ZwpTabletToolV2}, + zwp_tablet_v2::ZwpTabletV2, + }, viewporter::server::wp_viewporter::WpViewporter, }, xdg::{ @@ -338,6 +348,7 @@ impl Server { dh.create_global::(6, ()); dh.create_global::(5, ()); dh.create_global::(3, ()); + dh.create_global::(1, ()); global_noop!(ZwpLinuxDmabufV1); global_noop!(ZwpRelativePointerManagerV1); global_noop!(WpViewporter); @@ -641,6 +652,77 @@ simple_global_dispatch!(WlShm); simple_global_dispatch!(WlCompositor); simple_global_dispatch!(XdgWmBase); simple_global_dispatch!(ZxdgOutputManagerV1); +simple_global_dispatch!(ZwpTabletManagerV2); + +impl Dispatch for State { + fn request( + _: &mut Self, + client: &Client, + _: &ZwpTabletManagerV2, + request: ::Request, + _: &(), + dhandle: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + wayland_protocols::wp::tablet::zv2::server::zwp_tablet_manager_v2::Request::GetTabletSeat { tablet_seat, seat: _ } => { + let seat = data_init.init(tablet_seat, ()); + let tablet = client.create_resource::<_, _, State>(dhandle, 1, ()).unwrap(); + seat.tablet_added(&tablet); + tablet.name("tabby".to_owned()); + tablet.done(); + + let tool = client.create_resource::<_, _, State>(dhandle, 1, ()).unwrap(); + seat.tool_added(&tool); + tool._type(zwp_tablet_tool_v2::Type::Finger); + tool.done(); + + let pad = client.create_resource::<_, _, State>(dhandle, 1, ()).unwrap(); + let group = client.create_resource::<_, _, State>(dhandle, 1, ()).unwrap(); + let ring = client.create_resource::<_, _, State>(dhandle, 1, ()).unwrap(); + let strip = client.create_resource::<_, _, State>(dhandle, 1, ()).unwrap(); + + seat.pad_added(&pad); + pad.buttons(5); + pad.group(&group); + pad.done(); + + group.buttons(vec![]); + group.ring(&ring); + group.strip(&strip); + group.done(); + } + wayland_protocols::wp::tablet::zv2::server::zwp_tablet_manager_v2::Request::Destroy => {} + other => todo!("unhandled tablet manager request: {other:?}") + } + } +} + +macro_rules! unhandled { + ($type:ty) => { + impl Dispatch<$type, ()> for State { + fn request( + _: &mut Self, + _: &Client, + _: &$type, + _: <$type as Resource>::Request, + _: &(), + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + todo!(concat!(stringify!($type), " unhandled")); + } + } + }; +} + +unhandled!(ZwpTabletSeatV2); +unhandled!(ZwpTabletV2); +unhandled!(ZwpTabletToolV2); +unhandled!(ZwpTabletPadV2); +unhandled!(ZwpTabletPadGroupV2); +unhandled!(ZwpTabletPadRingV2); +unhandled!(ZwpTabletPadStripV2); impl Dispatch for State { fn request(