smithay/wayland/
xwayland_shell.rs

1//! Helpers for handling the xwayland shell protocol
2//!
3//! # Example
4//!
5//! ```
6//! use smithay::wayland::xwayland_shell::{
7//!     XWaylandShellHandler,
8//!     XWaylandShellState,
9//! };
10//! use smithay::delegate_xwayland_shell;
11//! use smithay::xwayland::xwm::{XwmId, X11Surface};
12//!
13//! # struct State;
14//! # let mut display = wayland_server::Display::<State>::new().unwrap();
15//! // Create the global:
16//! XWaylandShellState::new::<State>(
17//!     &display.handle(),
18//! );
19//! #
20//! # use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
21//!
22//! impl XWaylandShellHandler for State {
23//!     fn xwayland_shell_state(&mut self) -> &mut XWaylandShellState {
24//!         // Return a reference to the state we created earlier.
25//!         todo!()
26//!     }
27//!
28//!     fn surface_associated(&mut self, _xwm_id: XwmId, _surface: WlSurface, _window: X11Surface) {
29//!         // Called when XWayland has associated an X11 window with a wl_surface.
30//!         todo!()
31//!     }
32//! }
33//!
34//! #  use smithay::wayland::selection::SelectionTarget;
35//! #  use smithay::xwayland::{XWayland, XWaylandEvent, X11Wm, XwmHandler, xwm::{ResizeEdge, Reorder}};
36//! #  use smithay::utils::{Rectangle, Logical};
37//! #  use std::os::unix::io::OwnedFd;
38//! #  use std::process::Stdio;
39//!
40//! impl XwmHandler for State {
41//!     fn xwm_state(&mut self, xwm: XwmId) -> &mut X11Wm {
42//!         // ...
43//! #       unreachable!()
44//!     }
45//!     fn new_window(&mut self, xwm: XwmId, window: X11Surface) { /* ... */ }
46//!     fn new_override_redirect_window(&mut self, xwm: XwmId, window: X11Surface) { /* ... */ }
47//!     fn map_window_request(&mut self, xwm: XwmId, window: X11Surface) { /* ... */ }
48//!     fn mapped_override_redirect_window(&mut self, xwm: XwmId, window: X11Surface) { /* ... */ }
49//!     fn unmapped_window(&mut self, xwm: XwmId, window: X11Surface) { /* ... */ }
50//!     fn destroyed_window(&mut self, xwm: XwmId, window: X11Surface) { /* ... */ }
51//!     fn configure_request(&mut self, xwm: XwmId, window: X11Surface, x: Option<i32>, y: Option<i32>, w: Option<u32>, h: Option<u32>, reorder: Option<Reorder>) { /* ... */ }
52//!     fn configure_notify(&mut self, xwm: XwmId, window: X11Surface, geometry: Rectangle<i32, Logical>, above: Option<u32>) { /* ... */ }
53//!     fn resize_request(&mut self, xwm: XwmId, window: X11Surface, button: u32, resize_edge: ResizeEdge) { /* ... */ }
54//!     fn move_request(&mut self, xwm: XwmId, window: X11Surface, button: u32) { /* ... */ }
55//!     fn send_selection(&mut self, xwm: XwmId, selection: SelectionTarget, mime_type: String, fd: OwnedFd) { /* ... */ }
56//! }
57//!
58//! // implement Dispatch for your state.
59//! delegate_xwayland_shell!(State);
60//! ```
61
62use std::collections::HashMap;
63
64use tracing::{debug, warn};
65use wayland_protocols::xwayland::shell::v1::server::{
66    xwayland_shell_v1::{self, XwaylandShellV1},
67    xwayland_surface_v1::{self, XwaylandSurfaceV1},
68};
69use wayland_server::{
70    backend::GlobalId,
71    protocol::wl_surface::{self, WlSurface},
72    Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
73};
74
75use crate::{
76    wayland::compositor,
77    xwayland::{xwm::XwmId, X11Surface, XWaylandClientData, XwmHandler},
78};
79
80/// The role for an xwayland-associated surface.
81pub const XWAYLAND_SHELL_ROLE: &str = "xwayland_shell";
82
83const VERSION: u32 = 1;
84
85/// The global for the xwayland shell protocol.
86#[derive(Debug, Clone)]
87pub struct XWaylandShellState {
88    global: GlobalId,
89    by_serial: HashMap<u64, WlSurface>,
90}
91
92impl XWaylandShellState {
93    /// Registers a new [XwaylandShellV1] global. Only XWayland clients will be
94    /// able to bind it.
95    pub fn new<D>(display: &DisplayHandle) -> Self
96    where
97        D: GlobalDispatch<XwaylandShellV1, ()>,
98        D: Dispatch<XwaylandShellV1, ()>,
99        D: Dispatch<XwaylandSurfaceV1, XWaylandSurfaceUserData>,
100        D: 'static,
101    {
102        let global = display.create_global::<D, XwaylandShellV1, _>(VERSION, ());
103        Self {
104            global,
105            by_serial: HashMap::new(),
106        }
107    }
108
109    /// Retrieve a handle for the [XwaylandShellV1] global.
110    pub fn global(&self) -> GlobalId {
111        self.global.clone()
112    }
113
114    /// Retrieves the surface for a given serial.
115    pub fn surface_for_serial(&self, serial: u64) -> Option<WlSurface> {
116        self.by_serial.get(&serial).cloned()
117    }
118}
119
120/// Userdata for an xwayland shell surface.
121#[derive(Debug, Clone)]
122pub struct XWaylandSurfaceUserData {
123    pub(crate) wl_surface: wl_surface::WlSurface,
124}
125
126/// Handler for the xwayland shell protocol.
127pub trait XWaylandShellHandler {
128    /// Retrieves the global state.
129    fn xwayland_shell_state(&mut self) -> &mut XWaylandShellState;
130
131    /// An X11 window has been associated with a wayland surface. This doesn't
132    /// take effect until the wl_surface is committed.
133    fn surface_associated(&mut self, xwm: XwmId, wl_surface: wl_surface::WlSurface, surface: X11Surface) {
134        let _ = (xwm, wl_surface, surface);
135    }
136}
137
138/// Represents a pending X11 serial, used to associate X11 windows with wayland
139/// surfaces.
140#[derive(Debug, Default, Clone, Copy)]
141pub struct XWaylandShellCachedState {
142    /// The serial of the matching X11 window.
143    pub serial: Option<u64>,
144}
145
146impl compositor::Cacheable for XWaylandShellCachedState {
147    fn commit(&mut self, _dh: &DisplayHandle) -> Self {
148        *self
149    }
150
151    fn merge_into(self, into: &mut Self, _dh: &DisplayHandle) {
152        *into = self;
153    }
154}
155
156impl<D> GlobalDispatch<XwaylandShellV1, (), D> for XWaylandShellState
157where
158    D: GlobalDispatch<XwaylandShellV1, ()>,
159    D: Dispatch<XwaylandShellV1, ()>,
160    D: 'static,
161{
162    fn bind(
163        _state: &mut D,
164        _handle: &DisplayHandle,
165        _client: &Client,
166        resource: New<XwaylandShellV1>,
167        _global_data: &(),
168        data_init: &mut DataInit<'_, D>,
169    ) {
170        data_init.init(resource, ());
171    }
172
173    fn can_view(client: Client, _global_data: &()) -> bool {
174        client.get_data::<XWaylandClientData>().is_some()
175    }
176}
177
178impl<D> Dispatch<XwaylandShellV1, (), D> for XWaylandShellState
179where
180    D: Dispatch<XwaylandShellV1, ()>,
181    D: Dispatch<XwaylandSurfaceV1, XWaylandSurfaceUserData>,
182    D: XWaylandShellHandler + XwmHandler,
183    D: 'static,
184{
185    fn request(
186        _state: &mut D,
187        _client: &Client,
188        resource: &XwaylandShellV1,
189        request: <XwaylandShellV1 as Resource>::Request,
190        _data: &(),
191        _dhandle: &DisplayHandle,
192        data_init: &mut DataInit<'_, D>,
193    ) {
194        match request {
195            xwayland_shell_v1::Request::GetXwaylandSurface { id, surface } => {
196                if compositor::give_role(&surface, XWAYLAND_SHELL_ROLE).is_err() {
197                    resource.post_error(xwayland_shell_v1::Error::Role, "Surface already has a role.");
198                    return;
199                }
200
201                compositor::add_pre_commit_hook::<D, _>(&surface, serial_commit_hook);
202
203                data_init.init(id, XWaylandSurfaceUserData { wl_surface: surface });
204                // We call the handler callback once the serial is set.
205            }
206            xwayland_shell_v1::Request::Destroy => {
207                // The child objects created via this interface are unaffected.
208            }
209            _ => unreachable!(),
210        }
211    }
212}
213
214impl<D> Dispatch<XwaylandSurfaceV1, XWaylandSurfaceUserData, D> for XWaylandShellState
215where
216    D: Dispatch<XwaylandSurfaceV1, XWaylandSurfaceUserData>,
217    D: XWaylandShellHandler,
218    D: 'static,
219    D: XwmHandler,
220{
221    fn request(
222        _state: &mut D,
223        _client: &Client,
224        _resource: &XwaylandSurfaceV1,
225        request: <XwaylandSurfaceV1 as Resource>::Request,
226        data: &XWaylandSurfaceUserData,
227        _dhandle: &DisplayHandle,
228        _data_init: &mut DataInit<'_, D>,
229    ) {
230        match request {
231            // In order to match a WlSurface to an X11 window, we need to match a Serial sent both to the X11 window and the WlSurface.
232            // This can happen in any order so we need to store the Serial until we have both.
233            xwayland_surface_v1::Request::SetSerial { serial_lo, serial_hi } => {
234                let serial = u64::from(serial_lo) | (u64::from(serial_hi) << 32);
235
236                // Set the serial on the pending state of surface
237                compositor::with_states(&data.wl_surface, |states| {
238                    states
239                        .cached_state
240                        .get::<XWaylandShellCachedState>()
241                        .pending()
242                        .serial = Some(serial);
243                });
244            }
245            xwayland_surface_v1::Request::Destroy => {
246                // Any already existing associations are unaffected.
247            }
248            _ => unreachable!(),
249        }
250    }
251}
252
253fn serial_commit_hook<D: XWaylandShellHandler + XwmHandler + 'static>(
254    state: &mut D,
255    _dh: &DisplayHandle,
256    surface: &WlSurface,
257) {
258    if let Some(serial) = compositor::with_states(surface, |states| {
259        states
260            .cached_state
261            .get::<XWaylandShellCachedState>()
262            .pending()
263            .serial
264    }) {
265        if let Some(client) = surface.client() {
266            // We only care about surfaces created by XWayland.
267            if let Some(xwm_id) = client
268                .get_data::<XWaylandClientData>()
269                .and_then(|data| data.user_data().get::<XwmId>())
270            {
271                let xwm = XwmHandler::xwm_state(state, *xwm_id);
272
273                // This handles the case that the serial was set on the X11
274                // window before surface. To handle the other case, we look for
275                // a matching surface when the WL_SURFACE_SERIAL atom is sent.
276                if let Some(window) = xwm.unpaired_surfaces.remove(&serial) {
277                    if let Some(xsurface) = xwm
278                        .windows
279                        .iter()
280                        .find(|x| x.window_id() == window || x.mapped_window_id() == Some(window))
281                        .cloned()
282                    {
283                        debug!(
284                            window = xsurface.window_id(),
285                            wl_surface = ?surface.id().protocol_id(),
286                            "associated X11 window to wl_surface in commit hook",
287                        );
288
289                        xsurface.state.lock().unwrap().wl_surface = Some(surface.clone());
290
291                        XWaylandShellHandler::surface_associated(state, *xwm_id, surface.clone(), xsurface);
292                    } else {
293                        warn!(
294                            window,
295                            wl_surface = ?surface.id().protocol_id(),
296                            "Unknown X11 window associated to wl_surface in commit hook"
297                        )
298                    }
299                } else {
300                    // this is necessary for the atom-handler to look up the matching surface
301                    XWaylandShellHandler::xwayland_shell_state(state)
302                        .by_serial
303                        .insert(serial, surface.clone());
304                }
305            }
306        }
307    }
308}
309
310/// Macro to delegate implementation of the xwayland keyboard grab protocol
311#[macro_export]
312macro_rules! delegate_xwayland_shell {
313    ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
314        $crate::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
315            $crate::reexports::wayland_protocols::xwayland::shell::v1::server::xwayland_shell_v1::XwaylandShellV1: ()
316        ] => $crate::wayland::xwayland_shell::XWaylandShellState);
317        $crate::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
318            $crate::reexports::wayland_protocols::xwayland::shell::v1::server::xwayland_shell_v1::XwaylandShellV1: ()
319        ] => $crate::wayland::xwayland_shell::XWaylandShellState);
320        $crate::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
321            $crate::reexports::wayland_protocols::xwayland::shell::v1::server::xwayland_surface_v1::XwaylandSurfaceV1: $crate::wayland::xwayland_shell::XWaylandSurfaceUserData
322        ] => $crate::wayland::xwayland_shell::XWaylandShellState);
323    };
324}