Skip to main content

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}