From 548a2bf478068047191f77018bfb7867a13f8392 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Thu, 19 Mar 2026 18:51:38 +0100 Subject: [PATCH] wayland: implement wlr-virtual-pointer-unstable-v1 --- docs/features.md | 1 + jay-config/src/client.rs | 2 + src/client.rs | 2 + src/globals.rs | 2 + src/ifs.rs | 2 + src/ifs/wl_seat/event_handling.rs | 2 +- src/ifs/wl_seat/wl_pointer.rs | 2 +- src/ifs/zwlr_virtual_pointer_manager_v1.rs | 146 ++++++++++++ src/ifs/zwlr_virtual_pointer_v1.rs | 209 ++++++++++++++++++ .../src/config/parsers/capabilities.rs | 3 +- toml-spec/spec/spec.generated.json | 3 +- toml-spec/spec/spec.generated.md | 4 + toml-spec/spec/spec.yaml | 3 + wire/zwlr_virtual_pointer_manager_v1.txt | 13 ++ wire/zwlr_virtual_pointer_v1.txt | 47 ++++ 15 files changed, 437 insertions(+), 4 deletions(-) create mode 100644 src/ifs/zwlr_virtual_pointer_manager_v1.rs create mode 100644 src/ifs/zwlr_virtual_pointer_v1.rs create mode 100644 wire/zwlr_virtual_pointer_manager_v1.txt create mode 100644 wire/zwlr_virtual_pointer_v1.txt diff --git a/docs/features.md b/docs/features.md index 0ed92350..8daca46f 100644 --- a/docs/features.md +++ b/docs/features.md @@ -205,6 +205,7 @@ Jay supports the following wayland protocols: | zwlr_layer_shell_v1 | 5 | No[^lsaccess] | | zwlr_output_manager_v1 | 4 | Yes | | zwlr_screencopy_manager_v1 | 3 | Yes | +| zwlr_virtual_pointer_manager_v1 | 2 | Yes | | zwp_idle_inhibit_manager_v1 | 1 | | | zwp_input_method_manager_v2 | 1 | Yes | | zwp_linux_dmabuf_v1 | 5 | | diff --git a/jay-config/src/client.rs b/jay-config/src/client.rs index 507e39e3..7093687f 100644 --- a/jay-config/src/client.rs +++ b/jay-config/src/client.rs @@ -231,5 +231,7 @@ bitflags! { pub const CC_HEAD_MANAGER = 1 << 13, /// Grants access to the `zwlr_gamma_control_manager_v1` global. pub const CC_GAMMA_CONTROL_MANAGER = 1 << 14, + /// Grants access to the `zwlr_virtual_pointer_manager_v1` global. + pub const CC_VIRTUAL_POINTER = 1 << 15, } } diff --git a/src/client.rs b/src/client.rs index a1fa33cb..11c673a7 100644 --- a/src/client.rs +++ b/src/client.rs @@ -67,6 +67,7 @@ bitflags! { CAP_FOREIGN_TOPLEVEL_MANAGER = 1 << 12, CAP_HEAD_MANAGER = 1 << 13, CAP_GAMMA_CONTROL_MANAGER = 1 << 14, + CAP_VIRTUAL_POINTER_MANAGER = 1 << 15, } impl StaticText for ClientCapsEnum { @@ -87,6 +88,7 @@ impl StaticText for ClientCapsEnum { ClientCapsEnum::CAP_FOREIGN_TOPLEVEL_MANAGER => "foreign-toplevel-manager", ClientCapsEnum::CAP_HEAD_MANAGER => "head-manager", ClientCapsEnum::CAP_GAMMA_CONTROL_MANAGER => "gamma-control-manager", + ClientCapsEnum::CAP_VIRTUAL_POINTER_MANAGER => "virtual-pointer", } } } diff --git a/src/globals.rs b/src/globals.rs index ad4b64be..5e0d6208 100644 --- a/src/globals.rs +++ b/src/globals.rs @@ -68,6 +68,7 @@ use { zwlr_gamma_control_manager_v1::ZwlrGammaControlManagerV1Global, zwlr_layer_shell_v1::ZwlrLayerShellV1Global, zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1Global, + zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1Global, zwp_idle_inhibit_manager_v1::ZwpIdleInhibitManagerV1Global, zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1Global, zxdg_decoration_manager_v1::ZxdgDecorationManagerV1Global, @@ -248,6 +249,7 @@ singletons! { ZwpLinuxDmabufV1, WpLinuxDrmSyncobjManagerV1, WpPresentation, + ZwlrVirtualPointerManagerV1, } pub struct Globals { diff --git a/src/ifs.rs b/src/ifs.rs index f1168908..8c94796f 100644 --- a/src/ifs.rs +++ b/src/ifs.rs @@ -94,6 +94,8 @@ pub mod zwlr_gamma_control_v1; pub mod zwlr_layer_shell_v1; pub mod zwlr_screencopy_frame_v1; pub mod zwlr_screencopy_manager_v1; +pub mod zwlr_virtual_pointer_manager_v1; +pub mod zwlr_virtual_pointer_v1; pub mod zwp_idle_inhibit_manager_v1; pub mod zwp_linux_buffer_params_v1; pub mod zwp_linux_dmabuf_feedback_v1; diff --git a/src/ifs/wl_seat/event_handling.rs b/src/ifs/wl_seat/event_handling.rs index 9a98b58e..ef3448d3 100644 --- a/src/ifs/wl_seat/event_handling.rs +++ b/src/ifs/wl_seat/event_handling.rs @@ -669,7 +669,7 @@ impl WlSeatGlobal { self.cursor_moved(time_usec); } - fn motion_absolute_event( + pub fn motion_absolute_event( self: &Rc, time_usec: u64, rect: Rect, diff --git a/src/ifs/wl_seat/wl_pointer.rs b/src/ifs/wl_seat/wl_pointer.rs index bb6a3eb6..dbeed04e 100644 --- a/src/ifs/wl_seat/wl_pointer.rs +++ b/src/ifs/wl_seat/wl_pointer.rs @@ -15,7 +15,7 @@ use { #[expect(dead_code)] const ROLE: u32 = 0; -pub(super) const RELEASED: u32 = 0; +pub const RELEASED: u32 = 0; pub const PRESSED: u32 = 1; pub const VERTICAL_SCROLL: usize = 0; diff --git a/src/ifs/zwlr_virtual_pointer_manager_v1.rs b/src/ifs/zwlr_virtual_pointer_manager_v1.rs new file mode 100644 index 00000000..d36fbd17 --- /dev/null +++ b/src/ifs/zwlr_virtual_pointer_manager_v1.rs @@ -0,0 +1,146 @@ +use { + crate::{ + client::{CAP_VIRTUAL_POINTER_MANAGER, Client, ClientCaps, ClientError}, + globals::{Global, GlobalName}, + ifs::zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1, + leaks::Tracker, + object::{Object, Version}, + wire::{ + WlOutputId, WlSeatId, ZwlrVirtualPointerManagerV1Id, ZwlrVirtualPointerV1Id, + zwlr_virtual_pointer_manager_v1::*, + }, + }, + std::{ops::Deref, rc::Rc}, + thiserror::Error, +}; + +pub struct ZwlrVirtualPointerManagerV1Global { + name: GlobalName, +} + +impl ZwlrVirtualPointerManagerV1Global { + pub fn new(name: GlobalName) -> Self { + Self { name } + } + + fn bind_( + self: Rc, + id: ZwlrVirtualPointerManagerV1Id, + client: &Rc, + version: Version, + ) -> Result<(), ZwlrVirtualPointerManagerV1Error> { + let obj = Rc::new(ZwlrVirtualPointerManagerV1 { + id, + client: client.clone(), + tracker: Default::default(), + version, + }); + track!(client, obj); + client.add_client_obj(&obj)?; + Ok(()) + } +} + +global_base!( + ZwlrVirtualPointerManagerV1Global, + ZwlrVirtualPointerManagerV1, + ZwlrVirtualPointerManagerV1Error +); + +simple_add_global!(ZwlrVirtualPointerManagerV1Global); + +impl Global for ZwlrVirtualPointerManagerV1Global { + fn version(&self) -> u32 { + 2 + } + + fn required_caps(&self) -> ClientCaps { + CAP_VIRTUAL_POINTER_MANAGER + } +} + +pub struct ZwlrVirtualPointerManagerV1 { + pub id: ZwlrVirtualPointerManagerV1Id, + pub client: Rc, + pub tracker: Tracker, + pub version: Version, +} + +impl ZwlrVirtualPointerManagerV1 { + fn create_virtual_pointer( + &self, + id: ZwlrVirtualPointerV1Id, + seat: WlSeatId, + output: WlOutputId, + ) -> Result<(), ZwlrVirtualPointerManagerV1Error> { + let seat = if seat.is_some() { + self.client.lookup(seat)?.global.clone() + } else { + match self.client.state.seat_queue.last() { + None => return Err(ZwlrVirtualPointerManagerV1Error::NoSeat), + Some(s) => s.deref().clone(), + } + }; + let output = if output.is_none() { + None + } else { + Some(self.client.lookup(output)?.global.clone()) + }; + let obj = Rc::new(ZwlrVirtualPointerV1 { + id, + client: self.client.clone(), + tracker: Default::default(), + version: self.version, + events: Default::default(), + seat, + output, + buttons: Default::default(), + }); + track!(self.client, obj); + self.client.add_client_obj(&obj)?; + Ok(()) + } +} + +impl ZwlrVirtualPointerManagerV1RequestHandler for ZwlrVirtualPointerManagerV1 { + type Error = ZwlrVirtualPointerManagerV1Error; + + fn create_virtual_pointer( + &self, + req: CreateVirtualPointer, + _slf: &Rc, + ) -> Result<(), Self::Error> { + self.create_virtual_pointer(req.id, req.seat, WlOutputId::NONE) + } + + fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + self.client.remove_obj(self)?; + Ok(()) + } + + fn create_virtual_pointer_with_output( + &self, + req: CreateVirtualPointerWithOutput, + _slf: &Rc, + ) -> Result<(), Self::Error> { + self.create_virtual_pointer(req.id, req.seat, req.output) + } +} + +object_base! { + self = ZwlrVirtualPointerManagerV1; + version = self.version; +} + +impl Object for ZwlrVirtualPointerManagerV1 {} + +simple_add_obj!(ZwlrVirtualPointerManagerV1); + +#[derive(Debug, Error)] +pub enum ZwlrVirtualPointerManagerV1Error { + #[error(transparent)] + ClientError(Box), + #[error("There are no seats")] + NoSeat, +} +efrom!(ZwlrVirtualPointerManagerV1Error, ClientError); diff --git a/src/ifs/zwlr_virtual_pointer_v1.rs b/src/ifs/zwlr_virtual_pointer_v1.rs new file mode 100644 index 00000000..4c63ae8e --- /dev/null +++ b/src/ifs/zwlr_virtual_pointer_v1.rs @@ -0,0 +1,209 @@ +use { + crate::{ + backend::{AxisSource as BackendAxisSource, ButtonState, ScrollAxis}, + client::{Client, ClientError}, + fixed::Fixed, + ifs::{ + wl_output::OutputGlobalOpt, + wl_seat::{ + PX_PER_SCROLL, WlSeatGlobal, + wl_pointer::{self, CONTINUOUS, FINGER, PRESSED, RELEASED, WHEEL}, + }, + }, + leaks::Tracker, + object::{Object, Version}, + utils::{copyhashmap::CopyHashMap, syncqueue::SyncQueue}, + wire::{ZwlrVirtualPointerV1Id, zwlr_virtual_pointer_v1::*}, + }, + std::rc::Rc, + thiserror::Error, +}; + +pub struct ZwlrVirtualPointerV1 { + pub id: ZwlrVirtualPointerV1Id, + pub client: Rc, + pub tracker: Tracker, + pub version: Version, + pub events: SyncQueue, + pub seat: Rc, + pub output: Option>, + pub buttons: CopyHashMap, +} + +pub enum Event { + Motion(u32, Fixed, Fixed), + MotionAbsolute(u32, u32, u32, u32, u32), + Button(u32, u32, ButtonState), + Axis(u32, ScrollAxis, Fixed), + AxisSource(BackendAxisSource), + AxisStop(u32, ScrollAxis), + AxisDiscrete(u32, ScrollAxis, Fixed, i32), +} + +fn map_axis(axis: u32) -> Result { + const VERTICAL_SCROLL: u32 = wl_pointer::VERTICAL_SCROLL as u32; + const HORIZONTAL_SCROLL: u32 = wl_pointer::HORIZONTAL_SCROLL as u32; + let axis = match axis { + VERTICAL_SCROLL => ScrollAxis::Vertical, + HORIZONTAL_SCROLL => ScrollAxis::Horizontal, + n => return Err(ZwlrVirtualPointerV1Error::UnknownAxis(n)), + }; + Ok(axis) +} + +impl ZwlrVirtualPointerV1 { + fn detach(&self) { + for (button, _) in self.buttons.lock().drain() { + let now = self.client.state.now_usec(); + self.seat.button_event(now, button, ButtonState::Released); + } + } +} + +impl ZwlrVirtualPointerV1RequestHandler for ZwlrVirtualPointerV1 { + type Error = ZwlrVirtualPointerV1Error; + + fn motion(&self, req: Motion, _slf: &Rc) -> Result<(), Self::Error> { + self.events.push(Event::Motion(req.time, req.dx, req.dy)); + Ok(()) + } + + fn motion_absolute(&self, req: MotionAbsolute, _slf: &Rc) -> Result<(), Self::Error> { + self.events.push(Event::MotionAbsolute( + req.time, + req.x, + req.y, + req.x_extent, + req.y_extent, + )); + Ok(()) + } + + fn button(&self, req: Button, _slf: &Rc) -> Result<(), Self::Error> { + let state = match req.state { + RELEASED => ButtonState::Released, + PRESSED => ButtonState::Pressed, + n => return Err(ZwlrVirtualPointerV1Error::UnknownButtonState(n)), + }; + self.events.push(Event::Button(req.time, req.button, state)); + Ok(()) + } + + fn axis(&self, req: Axis, _slf: &Rc) -> Result<(), Self::Error> { + self.events + .push(Event::Axis(req.time, map_axis(req.axis)?, req.value)); + Ok(()) + } + + fn frame(&self, _req: Frame, _slf: &Rc) -> Result<(), Self::Error> { + fn ms_to_us(ms: u32) -> u64 { + ms as u64 * 1_000 + } + let mut axis_time = None; + while let Some(ev) = self.events.pop() { + match ev { + Event::Motion(time, dx, dy) => { + self.seat.motion_event(ms_to_us(time), dx, dy, dx, dy); + } + Event::MotionAbsolute(time, x, y, x_max, y_max) => { + let x = x as f32 / x_max as f32; + let y = y as f32 / y_max as f32; + let rect = self + .output + .as_ref() + .and_then(|c| c.get()) + .map(|g| g.pos.get()) + .unwrap_or_else(|| self.client.state.root.extents.get()); + self.seat.motion_absolute_event(ms_to_us(time), rect, x, y); + } + Event::Button(time, button, state) => { + match state { + ButtonState::Released => self.buttons.remove(&button), + ButtonState::Pressed => self.buttons.set(button, ()), + }; + self.seat.button_event(ms_to_us(time), button, state); + } + Event::Axis(time, axis, v) => { + axis_time = Some(time); + self.seat.axis_px(v, axis, false); + } + Event::AxisSource(source) => { + self.seat.axis_source(source); + } + Event::AxisStop(time, axis) => { + axis_time = Some(time); + self.seat.axis_stop(axis); + } + Event::AxisDiscrete(time, axis, value, discrete) => { + axis_time = Some(time); + self.seat.axis_px(value, axis, false); + self.seat + .axis_120(discrete.saturating_mul(120), axis, false); + } + } + } + if let Some(time) = axis_time { + self.seat.axis_frame(PX_PER_SCROLL, ms_to_us(time)); + } + Ok(()) + } + + fn axis_source(&self, req: AxisSource, _slf: &Rc) -> Result<(), Self::Error> { + let source = match req.axis_source { + WHEEL => BackendAxisSource::Wheel, + FINGER => BackendAxisSource::Finger, + CONTINUOUS => BackendAxisSource::Continuous, + n => return Err(ZwlrVirtualPointerV1Error::UnknownAxisSource(n)), + }; + self.events.push(Event::AxisSource(source)); + Ok(()) + } + + fn axis_stop(&self, req: AxisStop, _slf: &Rc) -> Result<(), Self::Error> { + self.events + .push(Event::AxisStop(req.time, map_axis(req.axis)?)); + Ok(()) + } + + fn axis_discrete(&self, req: AxisDiscrete, _slf: &Rc) -> Result<(), Self::Error> { + self.events.push(Event::AxisDiscrete( + req.time, + map_axis(req.axis)?, + req.value, + req.discrete, + )); + Ok(()) + } + + fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + self.client.remove_obj(self)?; + self.detach(); + Ok(()) + } +} + +object_base! { + self = ZwlrVirtualPointerV1; + version = self.version; +} + +impl Object for ZwlrVirtualPointerV1 { + fn break_loops(&self) { + self.detach(); + } +} + +simple_add_obj!(ZwlrVirtualPointerV1); + +#[derive(Debug, Error)] +pub enum ZwlrVirtualPointerV1Error { + #[error(transparent)] + ClientError(Box), + #[error("Unknown button state {0}")] + UnknownButtonState(u32), + #[error("Unknown axis {0}")] + UnknownAxis(u32), + #[error("Unknown axis source {0}")] + UnknownAxisSource(u32), +} +efrom!(ZwlrVirtualPointerV1Error, ClientError); diff --git a/toml-config/src/config/parsers/capabilities.rs b/toml-config/src/config/parsers/capabilities.rs index b17c641c..4cde9158 100644 --- a/toml-config/src/config/parsers/capabilities.rs +++ b/toml-config/src/config/parsers/capabilities.rs @@ -10,7 +10,7 @@ use { CC_DATA_CONTROL, CC_DRM_LEASE, CC_FOREIGN_TOPLEVEL_LIST, CC_FOREIGN_TOPLEVEL_MANAGER, CC_GAMMA_CONTROL_MANAGER, CC_HEAD_MANAGER, CC_IDLE_NOTIFIER, CC_INPUT_METHOD, CC_LAYER_SHELL, CC_SCREENCOPY, CC_SEAT_MANAGER, CC_SESSION_LOCK, CC_VIRTUAL_KEYBOARD, - CC_WORKSPACE_MANAGER, ClientCapabilities, + CC_VIRTUAL_POINTER, CC_WORKSPACE_MANAGER, ClientCapabilities, }, thiserror::Error, }; @@ -48,6 +48,7 @@ impl Parser for CapabilitiesParser { "foreign-toplevel-manager" => CC_FOREIGN_TOPLEVEL_MANAGER, "head-manager" => CC_HEAD_MANAGER, "gamma-control-manager" => CC_GAMMA_CONTROL_MANAGER, + "virtual-pointer" => CC_VIRTUAL_POINTER, _ => { return Err( CapabilitiesParserError::UnknownCapability(string.to_owned()).spanned(span), diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index b88c957e..73ac2e92 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -676,7 +676,8 @@ "workspace-manager", "foreign-toplevel-manager", "head-manager", - "gamma-control-manager" + "gamma-control-manager", + "virtual-pointer" ] }, { diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 82b5bf03..c6a32487 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -1066,6 +1066,10 @@ The string should have one of the following values: Grants access to the `zwlr_gamma_control_manager_v1` global. +- `virtual-pointer`: + + Grants access to the `zwlr_virtual_pointer_manager_v1` global. + #### An array diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 64eb1fde..38cfc7b8 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -4485,6 +4485,9 @@ ClientCapabilities: - value: gamma-control-manager description: | Grants access to the `zwlr_gamma_control_manager_v1` global. + - value: virtual-pointer + description: | + Grants access to the `zwlr_virtual_pointer_manager_v1` global. - kind: array description: An array of masks that are OR'd. items: diff --git a/wire/zwlr_virtual_pointer_manager_v1.txt b/wire/zwlr_virtual_pointer_manager_v1.txt new file mode 100644 index 00000000..b241eb4e --- /dev/null +++ b/wire/zwlr_virtual_pointer_manager_v1.txt @@ -0,0 +1,13 @@ +request create_virtual_pointer { + seat: id(wl_seat), + id: id(zwlr_virtual_pointer_v1) (new), +} + +request destroy (destructor, since = 1) { +} + +request create_virtual_pointer_with_output (since = 2) { + seat: id(wl_seat), + output: id(wl_output), + id: id(zwlr_virtual_pointer_v1) (new), +} diff --git a/wire/zwlr_virtual_pointer_v1.txt b/wire/zwlr_virtual_pointer_v1.txt new file mode 100644 index 00000000..48a76562 --- /dev/null +++ b/wire/zwlr_virtual_pointer_v1.txt @@ -0,0 +1,47 @@ +request motion { + time: u32, + dx: fixed, + dy: fixed, +} + +request motion_absolute { + time: u32, + x: u32, + y: u32, + x_extent: u32, + y_extent: u32, +} + +request button { + time: u32, + button: u32, + state: u32, +} + +request axis { + time: u32, + axis: u32, + value: fixed, +} + +request frame { +} + +request axis_source { + axis_source: u32, +} + +request axis_stop { + time: u32, + axis: u32, +} + +request axis_discrete { + time: u32, + axis: u32, + value: fixed, + discrete: i32, +} + +request destroy (destructor, since = 1) { +}