waydriver_input_mutter/lib.rs
1//! Mutter implementation of [`waydriver::InputBackend`].
2//!
3//! Wraps an `Arc<MutterState>` obtained from [`waydriver_compositor_mutter::MutterCompositor::state`]
4//! and sends keyboard / pointer events via
5//! `org.gnome.Mutter.RemoteDesktop.Session.{NotifyKeyboardKeysym, NotifyPointerMotionRelative}`.
6
7use std::sync::Arc;
8use std::time::Duration;
9
10use async_trait::async_trait;
11use tokio_util::sync::CancellationToken;
12
13use waydriver::backend::cancellable_tail;
14use waydriver::{Error, InputBackend, PointerAxis, PointerButton, Result};
15use waydriver_compositor_mutter::MutterState;
16
17/// Mutter RemoteDesktop input backend.
18pub struct MutterInput {
19 state: Arc<MutterState>,
20}
21
22impl MutterInput {
23 /// Create a new input backend from shared compositor state.
24 pub fn new(state: Arc<MutterState>) -> Self {
25 Self { state }
26 }
27
28 /// Issue a method call on `org.gnome.Mutter.RemoteDesktop.Session`
29 /// at the active session path, mapping any `zbus::Error` into a
30 /// `waydriver::Error::Process` tagged with `op` so the chain
31 /// reads "process: <op>: <zbus error>".
32 ///
33 /// Every input method (`NotifyKeyboardKeysym`,
34 /// `NotifyPointerButton`, `NotifyPointerMotionRelative/Absolute`,
35 /// `NotifyPointerAxisDiscrete`) is structurally identical apart
36 /// from the method name and the argument tuple — without this
37 /// helper each one repeated the same five-line `call_method`
38 /// invocation with the same destination/path/interface triple
39 /// hard-coded inline. Centralising means a future change to the
40 /// D-Bus API only edits one place, and the mapping from
41 /// "operation name" to error context is no longer scattered as
42 /// free-form literals.
43 async fn call_rd_session<Args>(
44 &self,
45 method: &'static str,
46 args: &Args,
47 op: &'static str,
48 ) -> Result<zbus::Message>
49 where
50 Args: serde::Serialize + zbus::zvariant::DynamicType,
51 {
52 self.state
53 .conn()
54 .call_method(
55 Some("org.gnome.Mutter.RemoteDesktop"),
56 self.state.rd_session_path(),
57 Some("org.gnome.Mutter.RemoteDesktop.Session"),
58 method,
59 args,
60 )
61 .await
62 .map_err(|e| Error::process_with(op, e))
63 }
64}
65
66#[async_trait]
67impl InputBackend for MutterInput {
68 async fn press_keysym(&self, keysym: u32, cancel: &CancellationToken) -> Result<()> {
69 self.key_down(keysym, cancel).await?;
70 // Mutter's RemoteDesktop needs a short gap between press and
71 // release or the app sees a 0ms keystroke that some handlers
72 // drop. This gap is *atomic* — we don't race it against the
73 // token, because cancelling here would leave the key stuck
74 // down in mutter's internal state with no scoped unwind.
75 tokio::time::sleep(Duration::from_millis(20)).await;
76 self.key_up(keysym, cancel).await?;
77 // Tail throttle so back-to-back calls from a test loop don't
78 // stack up faster than GTK can process them. The event already
79 // committed, so a cancelled session can cut this short.
80 cancellable_tail(Duration::from_millis(30), cancel).await;
81 Ok(())
82 }
83
84 async fn key_down(&self, keysym: u32, _cancel: &CancellationToken) -> Result<()> {
85 self.call_rd_session(
86 "NotifyKeyboardKeysym",
87 &(keysym, true),
88 "NotifyKeyboardKeysym press",
89 )
90 .await?;
91 Ok(())
92 }
93
94 async fn key_up(&self, keysym: u32, _cancel: &CancellationToken) -> Result<()> {
95 self.call_rd_session(
96 "NotifyKeyboardKeysym",
97 &(keysym, false),
98 "NotifyKeyboardKeysym release",
99 )
100 .await?;
101 Ok(())
102 }
103
104 async fn pointer_motion_relative(
105 &self,
106 dx: f64,
107 dy: f64,
108 _cancel: &CancellationToken,
109 ) -> Result<()> {
110 self.call_rd_session(
111 "NotifyPointerMotionRelative",
112 &(dx, dy),
113 "NotifyPointerMotionRelative",
114 )
115 .await?;
116 Ok(())
117 }
118
119 async fn pointer_motion_absolute(
120 &self,
121 x: f64,
122 y: f64,
123 _cancel: &CancellationToken,
124 ) -> Result<()> {
125 let stream = self
126 .state
127 .active_stream_path_lock()?
128 .clone()
129 .ok_or_else(|| {
130 Error::process("no active ScreenCast stream; absolute pointer motion needs one")
131 })?;
132 self.call_rd_session(
133 "NotifyPointerMotionAbsolute",
134 &(stream.as_str(), x, y),
135 "NotifyPointerMotionAbsolute",
136 )
137 .await?;
138 Ok(())
139 }
140
141 async fn pointer_button_down(
142 &self,
143 button: PointerButton,
144 _cancel: &CancellationToken,
145 ) -> Result<()> {
146 // Mutter's `NotifyPointerButton` takes the evdev code as `i32`.
147 // Named variants are <i32::MAX, but `PointerButton::Other(u32)`
148 // accepts the full `u32` range, so a fallible `try_from` is the
149 // only safe conversion at the boundary — `as i32` would silently
150 // wrap on values past `i32::MAX`.
151 let button = i32::try_from(button.evdev_code())
152 .map_err(|e| Error::process_with("NotifyPointerButton press", e))?;
153 self.call_rd_session(
154 "NotifyPointerButton",
155 &(button, true),
156 "NotifyPointerButton press",
157 )
158 .await?;
159 Ok(())
160 }
161
162 async fn pointer_button_up(
163 &self,
164 button: PointerButton,
165 cancel: &CancellationToken,
166 ) -> Result<()> {
167 let button = i32::try_from(button.evdev_code())
168 .map_err(|e| Error::process_with("NotifyPointerButton release", e))?;
169 self.call_rd_session(
170 "NotifyPointerButton",
171 &(button, false),
172 "NotifyPointerButton release",
173 )
174 .await?;
175 // Tail throttle — see press_keysym.
176 cancellable_tail(Duration::from_millis(30), cancel).await;
177 Ok(())
178 }
179
180 async fn pointer_axis_discrete(
181 &self,
182 axis: PointerAxis,
183 steps: i32,
184 cancel: &CancellationToken,
185 ) -> Result<()> {
186 // Mutter's `NotifyPointerAxisDiscrete` takes 0=vertical,
187 // 1=horizontal as a `u32`. This translation is the entire
188 // reason the trait surface is an enum: a future KWin/Sway
189 // backend can route differently here without changing the
190 // trait callers.
191 let axis_code: u32 = match axis {
192 PointerAxis::Vertical => 0,
193 PointerAxis::Horizontal => 1,
194 };
195 self.call_rd_session(
196 "NotifyPointerAxisDiscrete",
197 &(axis_code, steps),
198 "NotifyPointerAxisDiscrete",
199 )
200 .await?;
201 // Give GTK a beat to process the wheel event before the next call.
202 // Same rationale as the tail throttle in press_keysym —
203 // back-to-back axis events from a scroll loop can otherwise
204 // stack up faster than the compositor delivers them.
205 cancellable_tail(Duration::from_millis(30), cancel).await;
206 Ok(())
207 }
208}