Skip to main content

pixelflux/wayland/
xwm.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5 */
6
7//! XWayland for the compositor's own desktop: the `Xwayland` server spawned and kept running,
8//! and Smithay's X11 window manager driven as the window policy decides.
9//!
10//! Started only under a window policy (`Compositor::start_xwayland`, which `SELKIES_WM` on the
11//! Wayland backend asks for): the X11 clients become windows of the same `Space` as the Wayland
12//! ones (`Window::new_x11_window`), told to the policy through the same events with their
13//! ICCCM and EWMH details read into a `WindowInfo` (class and instance, transient parent, window
14//! type, size hints, Motif decorations, the states they asked for), and placed, framed, and
15//! stacked by the same commands. Override-redirect windows (menus, tooltips, drag icons) are
16//! placed where they ask, above everything, and never told to the policy, as X11 requires.
17//!
18//! Smithay's manager handles the selections (clipboard and primary between X11 and Wayland
19//! clients), XDND, `_NET_CLIENT_LIST`, `_NET_WM_STATE`, and `_NET_WM_MOVERESIZE`; what it does
20//! not publish and a desktop reads (`_NET_WORKAREA`, `_NET_ACTIVE_WINDOW`,
21//! `_NET_CLIENT_LIST_STACKING`, `_NET_FRAME_EXTENTS`) is written here over a second connection to
22//! the same server, since the manager's own is private to it, and so is the name it answers to
23//! on its check window (`publish_name`). A crashed Xwayland is started
24//! again after a short wait, so an X11 client started later still finds a server.
25//!
26//! Where the host has no DRM render node, the EGL vendor library a GPU container runtime injects
27//! (NVIDIA's) segfaults inside Xwayland's EGL initialization, and glamor would have nothing to
28//! accelerate there anyway, so Xwayland starts pinned to Mesa's EGL vendor, loading no external
29//! EGL platform, with its buffers in shared memory (`-shm`, `no_render_node_launch`). An
30//! Xwayland a caller ships beside its own executables is named by `PIXELFLUX_XWAYLAND`
31//! ([`XWAYLAND_VAR`]) and found ahead of the host's for that child alone.
32
33use std::collections::HashMap;
34use std::os::fd::OwnedFd;
35use std::sync::Arc;
36use std::time::{Duration, Instant};
37
38use smithay::desktop::Window;
39use smithay::input::Seat;
40use smithay::reexports::calloop::LoopHandle;
41use smithay::utils::{Logical, Point, Rectangle};
42use smithay::wayland::compositor::CompositorHandler;
43use smithay::wayland::selection::SelectionTarget;
44use smithay::wayland::selection::data_device::{
45    clear_data_device_selection, current_data_device_selection_userdata,
46    request_data_device_client_selection, set_data_device_selection,
47};
48use smithay::wayland::selection::primary_selection::{
49    clear_primary_selection, current_primary_selection_userdata, request_primary_client_selection,
50    set_primary_selection,
51};
52use smithay::wayland::xwayland_shell::{XWaylandShellHandler, XWaylandShellState};
53use smithay::xwayland::xwm::{
54    Reorder, ResizeEdge as X11ResizeEdge, WmWindowProperty, WmWindowType, XwmId,
55};
56use smithay::xwayland::{X11Surface, X11Wm, XWayland, XWaylandEvent, XwmHandler};
57use x11rb::connection::Connection;
58use x11rb::protocol::xproto::{self, AtomEnum, ConnectionExt, PropMode};
59use x11rb::rust_connection::RustConnection;
60use x11rb::wrapper::ConnectionExt as _;
61
62use crate::wayland::frontend::{AppState, FocusTarget, NEXT_WINDOW_ID, WindowMeta};
63use crate::wayland::wm;
64use crate::wm::{
65    Decoration, Rect, Request, ResizeEdge, Size, SizeHints, States, WindowId, WindowInfo,
66    WindowKind, WindowType,
67};
68
69/// How long a crashed Xwayland waits before it is started again.
70const RESTART_DELAY: Duration = Duration::from_secs(2);
71/// The glvnd vendor file of Mesa's EGL, the vendor Xwayland is pinned to without a render node.
72const MESA_EGL_VENDOR: &str = "/usr/share/glvnd/egl_vendor.d/50_mesa.json";
73
74/// The X11 side of the compositor.
75pub struct XwmState {
76    pub wm: Option<X11Wm>,
77    pub display: Option<u32>,
78    /// Who is told the display number when the server is up.
79    pub notify: Vec<std::sync::mpsc::Sender<u32>>,
80    /// Windows mapped through this manager, by the policy's id.
81    pub windows: HashMap<WindowId, Window>,
82    /// When a crashed server is started again.
83    pub restart_at: Option<Instant>,
84    /// The second connection the EWMH properties the manager does not write go over.
85    extras: Option<Extras>,
86    pub shell_state: XWaylandShellState,
87}
88
89impl XwmState {
90    pub fn new(shell_state: XWaylandShellState) -> XwmState {
91        XwmState {
92            wm: None,
93            display: None,
94            notify: Vec::new(),
95            windows: HashMap::new(),
96            restart_at: None,
97            extras: None,
98            shell_state,
99        }
100    }
101}
102
103x11rb::atom_manager! {
104    pub Atoms: AtomsCookie {
105        _NET_WORKAREA,
106        _NET_ACTIVE_WINDOW,
107        _NET_CLIENT_LIST_STACKING,
108        _NET_FRAME_EXTENTS,
109        _NET_SUPPORTED,
110        _NET_SUPPORTING_WM_CHECK,
111        _NET_WM_NAME,
112        UTF8_STRING,
113        _NET_WM_STATE_HIDDEN,
114        _NET_WM_ACTION_MOVE,
115        _NET_WM_ACTION_RESIZE,
116        _NET_WM_ACTION_MINIMIZE,
117        _NET_WM_ACTION_MAXIMIZE_HORZ,
118        _NET_WM_ACTION_MAXIMIZE_VERT,
119        _NET_WM_ACTION_FULLSCREEN,
120        _NET_WM_ACTION_CLOSE,
121        _NET_WM_ALLOWED_ACTIONS,
122    }
123}
124
125/// The second X connection and the atoms it writes with.
126struct Extras {
127    conn: RustConnection,
128    root: xproto::Window,
129    atoms: Atoms,
130}
131
132impl Extras {
133    fn connect(display: u32) -> Option<Extras> {
134        let (conn, screen) = RustConnection::connect(Some(&format!(":{display}"))).ok()?;
135        let root = conn.setup().roots.get(screen)?.root;
136        let atoms = Atoms::new(&conn).ok()?.reply().ok()?;
137        Some(Extras { conn, root, atoms })
138    }
139
140    fn set_cardinals(&self, window: xproto::Window, property: xproto::Atom, values: &[u32]) {
141        let _ = self.conn.change_property32(
142            PropMode::REPLACE,
143            window,
144            property,
145            AtomEnum::CARDINAL,
146            values,
147        );
148    }
149
150    fn set_windows(&self, window: xproto::Window, property: xproto::Atom, values: &[u32]) {
151        let _ = self.conn.change_property32(
152            PropMode::REPLACE,
153            window,
154            property,
155            AtomEnum::WINDOW,
156            values,
157        );
158    }
159
160    fn set_atoms(&self, window: xproto::Window, property: xproto::Atom, values: &[u32]) {
161        let _ = self.conn.change_property32(
162            PropMode::REPLACE,
163            window,
164            property,
165            AtomEnum::ATOM,
166            values,
167        );
168    }
169
170    fn flush(&self) {
171        let _ = self.conn.flush();
172    }
173}
174
175/// Where the Xwayland a caller ships is named, an executable called `Xwayland`; unset, the
176/// first on the `PATH`.
177pub const XWAYLAND_VAR: &str = "PIXELFLUX_XWAYLAND";
178
179/// The `PATH` Xwayland is looked up on: the directory of the one [`XWAYLAND_VAR`] names ahead of
180/// the process's own, so a bundle's Xwayland is found first for this child alone and the
181/// session's applications keep the host's programs; `None` where none is named or it is not
182/// there.
183fn xwayland_path(
184    named: Option<std::ffi::OsString>,
185    path: Option<std::ffi::OsString>,
186) -> Option<String> {
187    let named = std::path::PathBuf::from(named?);
188    let dir = named.parent()?.to_string_lossy().into_owned();
189    if !named.is_file() || dir.is_empty() {
190        return None;
191    }
192    let rest = path
193        .map(|p| p.to_string_lossy().into_owned())
194        .unwrap_or_default();
195    Some(if rest.is_empty() {
196        dir
197    } else {
198        format!("{dir}:{rest}")
199    })
200}
201
202/// The environment and arguments Xwayland starts with: the `PATH` that finds a named Xwayland
203/// ([`xwayland_path`]); nothing more where the host has a DRM render node, else Mesa's EGL
204/// vendor where it is installed, no external EGL platform, and `-shm`.
205fn no_render_node_launch() -> (Vec<(&'static str, String)>, Vec<&'static str>) {
206    let mut env = Vec::new();
207    if let Some(path) = xwayland_path(std::env::var_os(XWAYLAND_VAR), std::env::var_os("PATH")) {
208        env.push(("PATH", path));
209    }
210    let has_render_node = std::fs::read_dir("/dev/dri").is_ok_and(|dir| {
211        dir.flatten()
212            .any(|entry| entry.file_name().to_string_lossy().starts_with("renderD"))
213    });
214    if has_render_node {
215        return (env, Vec::new());
216    }
217    println!(
218        "[Wayland] No DRM render node: Xwayland starts on Mesa's EGL with shared-memory buffers."
219    );
220    env.push(("__EGL_EXTERNAL_PLATFORM_CONFIG_FILENAMES", String::new()));
221    if std::path::Path::new(MESA_EGL_VENDOR).exists() {
222        env.push((
223            "__EGL_VENDOR_LIBRARY_FILENAMES",
224            MESA_EGL_VENDOR.to_string(),
225        ));
226    }
227    (env, vec!["-shm"])
228}
229
230/// Spawn Xwayland and arm the start of its window manager once it is ready. `notify` is
231/// told the display number when it is.
232pub fn start(
233    state: &mut AppState,
234    handle: LoopHandle<'static, AppState>,
235    notify: Option<std::sync::mpsc::Sender<u32>>,
236) {
237    if let Some(notify) = notify {
238        state.xwm.notify.push(notify);
239    }
240    if state.xwm.wm.is_some() || state.xwm.restart_at.is_some() {
241        if let Some(display) = state.xwm.display {
242            for n in &state.xwm.notify {
243                let _ = n.send(display);
244            }
245            state.xwm.notify.clear();
246        }
247        return;
248    }
249    ensure_x11_socket_dir();
250    let (env, args) = no_render_node_launch();
251    let (xwayland, client) = match XWayland::spawn(
252        &state.dh,
253        None,
254        env,
255        args,
256        true,
257        std::process::Stdio::null(),
258        std::process::Stdio::null(),
259        |_| (),
260    ) {
261        Ok(spawned) => spawned,
262        Err(e) => {
263            eprintln!("[Wayland] Xwayland did not start: {e}; X11 clients have no server.");
264            state.xwm.restart_at = Some(Instant::now() + RESTART_DELAY);
265            return;
266        }
267    };
268    let dh = state.dh.clone();
269    let loop_handle = handle.clone();
270    let inserted = handle.insert_source(xwayland, move |event, _, state| match event {
271        XWaylandEvent::Ready {
272            x11_socket,
273            display_number,
274        } => {
275            state.client_compositor_state(&client).set_client_scale(1.0);
276            match X11Wm::start_wm(loop_handle.clone(), &dh, x11_socket, client.clone()) {
277                Ok(mut wm) => {
278                    let image = state.cursor_helper.get_image(1, Duration::ZERO);
279                    let _ = wm.set_cursor(
280                        &image.pixels_rgba,
281                        (image.width as u16, image.height as u16).into(),
282                        (image.xhot as u16, image.yhot as u16).into(),
283                    );
284                    state.xwm.wm = Some(wm);
285                    state.xwm.display = Some(display_number);
286                    state.xwm.extras = Extras::connect(display_number);
287                    if state.xwm.extras.is_none() {
288                        eprintln!("[Wayland] Xwayland :{display_number}: no second connection; _NET_WORKAREA and _NET_ACTIVE_WINDOW stay unset.");
289                    }
290                    publish_supported(state);
291                    publish_name(state);
292                    publish_workarea(state);
293                    println!("[Wayland] Xwayland on display :{display_number}; X11 clients are managed with the Wayland ones.");
294                    for n in state.xwm.notify.drain(..) {
295                        let _ = n.send(display_number);
296                    }
297                    wm::x11_display(state, display_number);
298                }
299                Err(e) => {
300                    eprintln!("[Wayland] Xwayland window manager did not start: {e}");
301                    state.xwm.restart_at = Some(Instant::now() + RESTART_DELAY);
302                }
303            }
304        }
305        XWaylandEvent::Error => {
306            eprintln!("[Wayland] Xwayland exited during startup; starting it again in {RESTART_DELAY:?}.");
307            state.xwm.wm = None;
308            state.xwm.display = None;
309            state.xwm.extras = None;
310            state.xwm.restart_at = Some(Instant::now() + RESTART_DELAY);
311        }
312    });
313    if let Err(e) = inserted {
314        eprintln!("[Wayland] Xwayland source not registered: {e}");
315    }
316}
317
318/// The directory X servers put their sockets in, which only an X server or a display manager
319/// creates: on a backend that runs no Xvfb nothing has, and Xwayland's sockets cannot be made
320/// without it.
321fn ensure_x11_socket_dir() {
322    use std::os::unix::fs::PermissionsExt;
323    let dir = std::path::Path::new("/tmp/.X11-unix");
324    if dir.is_dir() {
325        return;
326    }
327    if let Err(e) = std::fs::create_dir_all(dir) {
328        eprintln!("[Wayland] {}: not created: {e}", dir.display());
329        return;
330    }
331    let _ = std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o1777));
332}
333
334/// Start the server again where it exited and its wait passed; called on the frame timer.
335pub fn maintain(state: &mut AppState, handle: &LoopHandle<'static, AppState>, now: Instant) {
336    if state.xwm.wm.is_none() && state.xwm.restart_at.is_some_and(|at| at <= now) {
337        state.xwm.restart_at = None;
338        start(state, handle.clone(), None);
339    }
340}
341
342/// The policy's window for an X11 surface.
343fn window_of(state: &AppState, surface: &X11Surface) -> Option<(WindowId, Window)> {
344    state
345        .xwm
346        .windows
347        .iter()
348        .find(|(_, w)| w.x11_surface() == Some(surface))
349        .map(|(id, w)| (*id, w.clone()))
350}
351
352fn window_type_of(surface: &X11Surface) -> WindowType {
353    match surface.window_type() {
354        Some(WmWindowType::Dialog) => WindowType::Dialog,
355        Some(WmWindowType::Utility) => WindowType::Utility,
356        Some(WmWindowType::Toolbar) => WindowType::Toolbar,
357        Some(WmWindowType::Menu) => WindowType::Menu,
358        Some(WmWindowType::Splash) => WindowType::Splash,
359        Some(WmWindowType::Dock) => WindowType::Dock,
360        Some(WmWindowType::Desktop) => WindowType::Desktop,
361        Some(WmWindowType::Notification) => WindowType::Notification,
362        Some(WmWindowType::DropdownMenu) => WindowType::DropdownMenu,
363        Some(WmWindowType::PopupMenu) => WindowType::PopupMenu,
364        Some(WmWindowType::Tooltip) => WindowType::Tooltip,
365        Some(WmWindowType::Combo) => WindowType::Combo,
366        Some(WmWindowType::Dnd) => WindowType::Dnd,
367        Some(WmWindowType::Normal) | None => {
368            if surface.is_transient_for().is_some() {
369                WindowType::Dialog
370            } else {
371                WindowType::Normal
372            }
373        }
374    }
375}
376
377/// What the policy is told about an X11 window.
378pub fn window_info(
379    state: &AppState,
380    id: WindowId,
381    surface: &X11Surface,
382    output: u32,
383    _decoration: Decoration,
384) -> WindowInfo {
385    let parent = surface.is_transient_for().and_then(|parent| {
386        state
387            .xwm
388            .windows
389            .iter()
390            .find(|(_, w)| w.x11_surface().is_some_and(|s| s.window_id() == parent))
391            .map(|(id, _)| *id)
392    });
393    let hints = surface.size_hints();
394    let geometry = surface.geometry();
395    let decoration = if surface.is_decorated() {
396        Decoration::None
397    } else {
398        Decoration::Server
399    };
400    let accepts_focus = !matches!(
401        surface.input_model(),
402        smithay::xwayland::xwm::WmInputModel::None
403    );
404    WindowInfo {
405        id,
406        kind: Some(WindowKind::X11 {
407            override_redirect: surface.is_override_redirect(),
408        }),
409        window_type: window_type_of(surface),
410        title: surface.title(),
411        app_id: surface.class(),
412        instance: surface.instance(),
413        parent,
414        modal: surface.is_modal(),
415        hints: SizeHints {
416            min: surface.min_size().map(|s| Size { w: s.w, h: s.h }),
417            max: surface.max_size().map(|s| Size { w: s.w, h: s.h }),
418            increment: hints
419                .as_ref()
420                .and_then(|h| h.size_increment)
421                .map(|(w, h)| Size { w, h }),
422            base: surface.base_size().map(|s| Size { w: s.w, h: s.h }),
423        },
424        requested: Some(Rect::new(
425            geometry.loc.x,
426            geometry.loc.y,
427            geometry.size.w,
428            geometry.size.h,
429        )),
430        positioned: hints
431            .as_ref()
432            .is_some_and(|h| h.position.is_some() && (geometry.loc.x != 0 || geometry.loc.y != 0))
433            || !matches!(
434                window_type_of(surface),
435                WindowType::Normal | WindowType::Dialog | WindowType::Utility | WindowType::Toolbar
436            ),
437        decoration,
438        output,
439        pid: surface.pid(),
440        wants_fullscreen: surface.is_fullscreen(),
441        wants_maximized: surface.is_maximized(),
442        wants_minimized: false,
443        skip_taskbar: surface.is_skip_taskbar(),
444        accepts_focus,
445    }
446}
447
448/// Carry a configure out on an X11 window: its geometry and its states.
449pub fn configure(surface: &X11Surface, rect: Rect, states: States) {
450    if rect.w > 0 && rect.h > 0 {
451        let _ = surface.configure(Some(Rectangle::<i32, Logical>::new(
452            (rect.x, rect.y).into(),
453            (rect.w, rect.h).into(),
454        )));
455    }
456    let _ = surface.set_maximized(states.maximized);
457    let _ = surface.set_fullscreen(states.fullscreen);
458    let _ = surface.set_activated(states.activated);
459    let _ = surface.set_hidden(states.minimized || states.hidden);
460}
461
462/// Publish what the manager supports beyond Smithay's own list, so clients ask for it.
463fn publish_supported(state: &mut AppState) {
464    let Some(extras) = state.xwm.extras.as_ref() else {
465        return;
466    };
467    let a = &extras.atoms;
468    let supported = [
469        a._NET_WORKAREA,
470        a._NET_ACTIVE_WINDOW,
471        a._NET_CLIENT_LIST_STACKING,
472        a._NET_FRAME_EXTENTS,
473        a._NET_WM_STATE_HIDDEN,
474        a._NET_WM_ALLOWED_ACTIONS,
475    ];
476    let existing: Vec<u32> = extras
477        .conn
478        .get_property(
479            false,
480            extras.root,
481            a._NET_SUPPORTED,
482            AtomEnum::ATOM,
483            0,
484            1024,
485        )
486        .ok()
487        .and_then(|c| c.reply().ok())
488        .and_then(|r| r.value32().map(|v| v.collect()))
489        .unwrap_or_default();
490    let mut all = existing;
491    for atom in supported {
492        if !all.contains(&atom) {
493            all.push(atom);
494        }
495    }
496    extras.set_atoms(extras.root, a._NET_SUPPORTED, &all);
497    extras.flush();
498}
499
500/// The manager's name on the supporting check window, which clients read to decide how to
501/// behave: Chromium and Electron draw their own frame only under a window manager they know by
502/// this name and ask for a server frame under any other, Smithay's own among them, so the
503/// manager answers to the name the X11 backend's manager gives, the one whose behavior the
504/// policy carries.
505const NET_WM_NAME: &str = "Openbox";
506
507/// Rename Smithay's supporting check window to [`NET_WM_NAME`].
508fn publish_name(state: &mut AppState) {
509    let Some(extras) = state.xwm.extras.as_ref() else {
510        return;
511    };
512    let a = &extras.atoms;
513    let check = extras
514        .conn
515        .get_property(
516            false,
517            extras.root,
518            a._NET_SUPPORTING_WM_CHECK,
519            AtomEnum::WINDOW,
520            0,
521            1,
522        )
523        .ok()
524        .and_then(|c| c.reply().ok())
525        .and_then(|r| r.value32().and_then(|mut v| v.next()));
526    let Some(check) = check else {
527        return;
528    };
529    let _ = extras.conn.change_property8(
530        PropMode::REPLACE,
531        check,
532        a._NET_WM_NAME,
533        a.UTF8_STRING,
534        NET_WM_NAME.as_bytes(),
535    );
536    extras.flush();
537}
538
539/// `_NET_WORKAREA` from the policy's outputs: the primary's workarea, as one desktop.
540pub fn publish_workarea(state: &mut AppState) {
541    let Some(extras) = state.xwm.extras.as_ref() else {
542        return;
543    };
544    let workarea = state
545        .wm
546        .as_ref()
547        .and_then(|wm| {
548            wm.outputs
549                .iter()
550                .find(|o| o.primary)
551                .or(wm.outputs.first())
552                .map(|o| o.workarea)
553        })
554        .or_else(|| {
555            state
556                .output_nodes
557                .first()
558                .and_then(|n| n.logical_geometry())
559                .map(|g| Rect::new(g.loc.x, g.loc.y, g.size.w, g.size.h))
560        });
561    if let Some(w) = workarea {
562        extras.set_cardinals(
563            extras.root,
564            extras.atoms._NET_WORKAREA,
565            &[
566                w.x.max(0) as u32,
567                w.y.max(0) as u32,
568                w.w.max(0) as u32,
569                w.h.max(0) as u32,
570            ],
571        );
572        extras.flush();
573    }
574}
575
576/// `_NET_ACTIVE_WINDOW` and `_NET_CLIENT_LIST_STACKING`, after a focus or stacking change.
577pub fn publish_stacking(state: &mut AppState) {
578    let Some(extras) = state.xwm.extras.as_ref() else {
579        return;
580    };
581    let active = state
582        .keyboard_focus
583        .and_then(|id| state.xwm.windows.get(&id))
584        .and_then(|w| w.x11_surface().map(|s| s.window_id()))
585        .unwrap_or(0);
586    extras.set_windows(extras.root, extras.atoms._NET_ACTIVE_WINDOW, &[active]);
587    let stacking: Vec<u32> = state
588        .space
589        .elements()
590        .filter_map(|w| w.x11_surface().map(|s| s.window_id()))
591        .collect();
592    extras.set_windows(
593        extras.root,
594        extras.atoms._NET_CLIENT_LIST_STACKING,
595        &stacking,
596    );
597    extras.flush();
598}
599
600/// `_NET_FRAME_EXTENTS` and `_NET_WM_ALLOWED_ACTIONS` on a window the policy framed.
601pub fn publish_frame(state: &mut AppState, id: WindowId) {
602    let Some(extras) = state.xwm.extras.as_ref() else {
603        return;
604    };
605    let Some(window) = state.xwm.windows.get(&id) else {
606        return;
607    };
608    let Some(surface) = window.x11_surface() else {
609        return;
610    };
611    let e = wm::frame_extents(state, id);
612    extras.set_cardinals(
613        surface.window_id(),
614        extras.atoms._NET_FRAME_EXTENTS,
615        &[e.left as u32, e.right as u32, e.top as u32, e.bottom as u32],
616    );
617    let a = &extras.atoms;
618    extras.set_atoms(
619        surface.window_id(),
620        a._NET_WM_ALLOWED_ACTIONS,
621        &[
622            a._NET_WM_ACTION_MOVE,
623            a._NET_WM_ACTION_RESIZE,
624            a._NET_WM_ACTION_MINIMIZE,
625            a._NET_WM_ACTION_MAXIMIZE_HORZ,
626            a._NET_WM_ACTION_MAXIMIZE_VERT,
627            a._NET_WM_ACTION_FULLSCREEN,
628            a._NET_WM_ACTION_CLOSE,
629        ],
630    );
631    extras.flush();
632}
633
634/// The end of a drag-and-drop grab, an X11 client's or a Wayland client's: the drag's icon is
635/// no longer drawn.
636impl smithay::input::dnd::DndGrabHandler for AppState {
637    fn dropped(
638        &mut self,
639        _target: Option<smithay::input::dnd::DndTarget<'_, Self>>,
640        _validated: bool,
641        _seat: Seat<Self>,
642        _location: Point<f64, Logical>,
643    ) {
644        self.dnd_icon = None;
645    }
646
647    fn cancelled(&mut self, _seat: Seat<Self>, _location: Point<f64, Logical>) {
648        self.dnd_icon = None;
649    }
650}
651
652impl XWaylandShellHandler for AppState {
653    fn xwayland_shell_state(&mut self) -> &mut XWaylandShellState {
654        &mut self.xwm.shell_state
655    }
656}
657
658impl XwmHandler for AppState {
659    fn xwm_state(&mut self, _xwm: XwmId) -> &mut X11Wm {
660        self.xwm
661            .wm
662            .as_mut()
663            .expect("the X11 window manager that called")
664    }
665
666    fn new_window(&mut self, _xwm: XwmId, _window: X11Surface) {}
667
668    fn new_override_redirect_window(&mut self, _xwm: XwmId, _window: X11Surface) {}
669
670    fn map_window_request(&mut self, _xwm: XwmId, surface: X11Surface) {
671        let output = self.pointer_display();
672        let window = Window::new_x11_window(surface.clone());
673        let id = NEXT_WINDOW_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
674        window
675            .user_data()
676            .insert_if_missing_threadsafe(|| WindowMeta {
677                id,
678                output: std::sync::atomic::AtomicU32::new(output),
679                placed: std::sync::atomic::AtomicBool::new(true),
680                parked: std::sync::atomic::AtomicBool::new(false),
681            });
682        self.xwm.windows.insert(id, window.clone());
683        let _ = surface.set_mapped(true);
684        if wm::active(self) {
685            wm::window_added(self, &window);
686        }
687        let rect = wm::rect_of(self, id)
688            .filter(|r| r.w > 0 && r.h > 0)
689            .unwrap_or_else(|| {
690                let g = surface.geometry();
691                Rect::new(g.loc.x, g.loc.y, g.size.w.max(1), g.size.h.max(1))
692            });
693        let _ = surface.configure(Some(Rectangle::<i32, Logical>::new(
694            (rect.x, rect.y).into(),
695            (rect.w, rect.h).into(),
696        )));
697        self.space
698            .map_element(window.clone(), (rect.x, rect.y), true);
699        wm::window_mapped(
700            self,
701            &window,
702            Size {
703                w: rect.w,
704                h: rect.h,
705            },
706        );
707        crate::wayland::foreign_toplevel::add(self, &window);
708        publish_frame(self, id);
709        publish_stacking(self);
710        if !wm::active(self)
711            && let Some(keyboard) = self.seat.get_keyboard()
712        {
713            keyboard.set_focus(
714                self,
715                Some(FocusTarget::Window(window)),
716                crate::wayland::frontend::next_serial(),
717            );
718        }
719        self.refocus_pointer();
720    }
721
722    fn map_window_notify(&mut self, _xwm: XwmId, _window: X11Surface) {}
723
724    fn mapped_override_redirect_window(&mut self, _xwm: XwmId, surface: X11Surface) {
725        let location = surface.geometry().loc;
726        let window = Window::new_x11_window(surface);
727        window
728            .user_data()
729            .insert_if_missing_threadsafe(|| WindowMeta {
730                id: NEXT_WINDOW_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
731                output: std::sync::atomic::AtomicU32::new(self.pointer_display()),
732                placed: std::sync::atomic::AtomicBool::new(true),
733                parked: std::sync::atomic::AtomicBool::new(false),
734            });
735        self.space.map_element(window, location, true);
736        self.refocus_pointer();
737        for node in &mut self.output_nodes {
738            if let Some(cap) = node.capture.as_mut() {
739                cap.change_pending = true;
740            }
741        }
742    }
743
744    fn unmapped_window(&mut self, _xwm: XwmId, surface: X11Surface) {
745        let found = self
746            .space
747            .elements()
748            .find(|w| w.x11_surface() == Some(&surface))
749            .cloned();
750        if let Some(window) = found {
751            wm::snapshot(self, &window);
752            self.space.unmap_elem(&window);
753        }
754        if let Some((id, window)) = window_of(self, &surface) {
755            wm::window_unmapped(self, &window);
756            crate::wayland::foreign_toplevel::remove(self, id);
757            if let Some(wm) = self.wm.as_mut() {
758                wm.hidden.remove(&id);
759            }
760        }
761        if !surface.is_override_redirect() {
762            let _ = surface.set_mapped(false);
763        }
764        self.refocus_pointer();
765        publish_stacking(self);
766    }
767
768    fn destroyed_window(&mut self, _xwm: XwmId, surface: X11Surface) {
769        if let Some((id, _)) = window_of(self, &surface) {
770            self.xwm.windows.remove(&id);
771            wm::window_removed(self, id);
772            crate::wayland::foreign_toplevel::remove(self, id);
773        }
774    }
775
776    fn configure_request(
777        &mut self,
778        _xwm: XwmId,
779        surface: X11Surface,
780        x: Option<i32>,
781        y: Option<i32>,
782        w: Option<u32>,
783        h: Option<u32>,
784        reorder: Option<Reorder>,
785    ) {
786        let mut geo = surface.geometry();
787        if let Some(x) = x {
788            geo.loc.x = x;
789        }
790        if let Some(y) = y {
791            geo.loc.y = y;
792        }
793        if let Some(w) = w {
794            geo.size.w = w as i32;
795        }
796        if let Some(h) = h {
797            geo.size.h = h as i32;
798        }
799        match window_of(self, &surface) {
800            Some((id, _)) if wm::active(self) => {
801                wm::request(
802                    self,
803                    id,
804                    Request::Geometry(Rect::new(geo.loc.x, geo.loc.y, geo.size.w, geo.size.h)),
805                );
806                match reorder {
807                    Some(Reorder::Top) => {
808                        wm::request(self, id, Request::Raise);
809                    }
810                    Some(Reorder::Bottom) => {
811                        wm::request(self, id, Request::Lower);
812                    }
813                    _ => {}
814                }
815            }
816            _ => {
817                let _ = surface.configure(Some(geo));
818            }
819        }
820    }
821
822    fn configure_notify(
823        &mut self,
824        _xwm: XwmId,
825        surface: X11Surface,
826        geometry: Rectangle<i32, Logical>,
827        _above: Option<u32>,
828    ) {
829        if !surface.is_override_redirect() {
830            return;
831        }
832        let found = self
833            .space
834            .elements()
835            .find(|w| w.x11_surface() == Some(&surface))
836            .cloned();
837        if let Some(window) = found {
838            self.space.map_element(window, geometry.loc, false);
839            for node in &mut self.output_nodes {
840                if let Some(cap) = node.capture.as_mut() {
841                    cap.change_pending = true;
842                }
843            }
844        }
845    }
846
847    fn property_notify(&mut self, _xwm: XwmId, surface: X11Surface, property: WmWindowProperty) {
848        if matches!(
849            property,
850            WmWindowProperty::Title
851                | WmWindowProperty::Class
852                | WmWindowProperty::TransientFor
853                | WmWindowProperty::WindowType
854                | WmWindowProperty::MotifHints
855                | WmWindowProperty::NormalHints
856                | WmWindowProperty::Hints
857        ) && let Some((id, window)) = window_of(self, &surface)
858        {
859            wm::window_changed(self, &window);
860            crate::wayland::foreign_toplevel::update(self, id);
861        }
862    }
863
864    fn maximize_request(&mut self, _xwm: XwmId, surface: X11Surface) {
865        if let Some((id, _)) = window_of(self, &surface) {
866            wm::request(self, id, Request::Maximize);
867        }
868    }
869
870    fn unmaximize_request(&mut self, _xwm: XwmId, surface: X11Surface) {
871        if let Some((id, _)) = window_of(self, &surface) {
872            wm::request(self, id, Request::Unmaximize);
873        }
874    }
875
876    fn fullscreen_request(&mut self, _xwm: XwmId, surface: X11Surface) {
877        if let Some((id, _)) = window_of(self, &surface) {
878            wm::request(self, id, Request::Fullscreen(None));
879        }
880    }
881
882    fn unfullscreen_request(&mut self, _xwm: XwmId, surface: X11Surface) {
883        if let Some((id, _)) = window_of(self, &surface) {
884            wm::request(self, id, Request::Unfullscreen);
885        }
886    }
887
888    fn minimize_request(&mut self, _xwm: XwmId, surface: X11Surface) {
889        if let Some((id, _)) = window_of(self, &surface) {
890            wm::request(self, id, Request::Minimize);
891        }
892    }
893
894    fn unminimize_request(&mut self, _xwm: XwmId, surface: X11Surface) {
895        if let Some((id, _)) = window_of(self, &surface) {
896            wm::request(self, id, Request::Unminimize);
897        }
898    }
899
900    fn above_request(&mut self, _xwm: XwmId, surface: X11Surface) {
901        if let Some((id, _)) = window_of(self, &surface) {
902            wm::request(self, id, Request::Above(true));
903        }
904    }
905
906    fn unabove_request(&mut self, _xwm: XwmId, surface: X11Surface) {
907        if let Some((id, _)) = window_of(self, &surface) {
908            wm::request(self, id, Request::Above(false));
909        }
910    }
911
912    fn below_request(&mut self, _xwm: XwmId, surface: X11Surface) {
913        if let Some((id, _)) = window_of(self, &surface) {
914            wm::request(self, id, Request::Below(true));
915        }
916    }
917
918    fn unbelow_request(&mut self, _xwm: XwmId, surface: X11Surface) {
919        if let Some((id, _)) = window_of(self, &surface) {
920            wm::request(self, id, Request::Below(false));
921        }
922    }
923
924    fn shade_request(&mut self, _xwm: XwmId, surface: X11Surface) {
925        if let Some((id, _)) = window_of(self, &surface) {
926            wm::request(self, id, Request::Shade(true));
927        }
928    }
929
930    fn unshade_request(&mut self, _xwm: XwmId, surface: X11Surface) {
931        if let Some((id, _)) = window_of(self, &surface) {
932            wm::request(self, id, Request::Shade(false));
933        }
934    }
935
936    fn demands_attention_request(&mut self, _xwm: XwmId, surface: X11Surface) {
937        if let Some((id, _)) = window_of(self, &surface) {
938            wm::request(self, id, Request::Attention(true));
939        }
940    }
941
942    fn undemands_attention_request(&mut self, _xwm: XwmId, surface: X11Surface) {
943        if let Some((id, _)) = window_of(self, &surface) {
944            wm::request(self, id, Request::Attention(false));
945        }
946    }
947
948    fn resize_request(
949        &mut self,
950        _xwm: XwmId,
951        surface: X11Surface,
952        _button: u32,
953        edges: X11ResizeEdge,
954    ) {
955        let Some((id, _)) = window_of(self, &surface) else {
956            return;
957        };
958        let edge = match edges {
959            X11ResizeEdge::Top => ResizeEdge::Top,
960            X11ResizeEdge::Bottom => ResizeEdge::Bottom,
961            X11ResizeEdge::Left => ResizeEdge::Left,
962            X11ResizeEdge::Right => ResizeEdge::Right,
963            X11ResizeEdge::TopLeft => ResizeEdge::TopLeft,
964            X11ResizeEdge::TopRight => ResizeEdge::TopRight,
965            X11ResizeEdge::BottomLeft => ResizeEdge::BottomLeft,
966            X11ResizeEdge::BottomRight => ResizeEdge::BottomRight,
967        };
968        wm::request(self, id, Request::Resize(edge));
969    }
970
971    fn move_request(&mut self, _xwm: XwmId, surface: X11Surface, _button: u32) {
972        if let Some((id, _)) = window_of(self, &surface) {
973            wm::request(self, id, Request::Move);
974        }
975    }
976
977    fn active_window_request(
978        &mut self,
979        _xwm: XwmId,
980        surface: X11Surface,
981        _timestamp: u32,
982        _current: Option<X11Surface>,
983    ) {
984        if let Some((id, _)) = window_of(self, &surface) {
985            wm::request(self, id, Request::Activate);
986        }
987    }
988
989    fn allow_selection_access(&mut self, xwm: XwmId, _selection: SelectionTarget) -> bool {
990        self.seat
991            .get_keyboard()
992            .and_then(|k| k.current_focus())
993            .is_some_and(|f| matches!(f, FocusTarget::Window(w) if w.x11_surface().and_then(|s| s.xwm_id()) == Some(xwm)))
994    }
995
996    fn send_selection(
997        &mut self,
998        _xwm: XwmId,
999        selection: SelectionTarget,
1000        mime_type: String,
1001        fd: OwnedFd,
1002    ) {
1003        match selection {
1004            SelectionTarget::Clipboard => {
1005                let _ = request_data_device_client_selection(&self.seat, mime_type, fd);
1006            }
1007            SelectionTarget::Primary => {
1008                let _ = request_primary_client_selection(&self.seat, mime_type, fd);
1009            }
1010        }
1011    }
1012
1013    fn new_selection(&mut self, _xwm: XwmId, selection: SelectionTarget, mime_types: Vec<String>) {
1014        let dh = self.dh.clone();
1015        match selection {
1016            SelectionTarget::Clipboard => {
1017                let flavors = crate::wayland::frontend::clipboard_flavors(&mime_types);
1018                set_data_device_selection(&dh, &self.seat, mime_types, Arc::new(Vec::new()));
1019                self.current_selection_mimes.clone_from(&flavors);
1020                if self.clipboard_callback.is_some() {
1021                    self.pending_clipboard_read = Some(flavors);
1022                }
1023            }
1024            SelectionTarget::Primary => {
1025                set_primary_selection(&dh, &self.seat, mime_types, Arc::new(Vec::new()));
1026            }
1027        }
1028    }
1029
1030    fn cleared_selection(&mut self, _xwm: XwmId, selection: SelectionTarget) {
1031        let dh = self.dh.clone();
1032        match selection {
1033            SelectionTarget::Clipboard => {
1034                if current_data_device_selection_userdata(&self.seat).is_some() {
1035                    clear_data_device_selection(&dh, &self.seat);
1036                }
1037            }
1038            SelectionTarget::Primary => {
1039                if current_primary_selection_userdata(&self.seat).is_some() {
1040                    clear_primary_selection(&dh, &self.seat);
1041                }
1042            }
1043        }
1044    }
1045
1046    fn show_desktop_request(&mut self, _xwm: XwmId) {
1047        wm::show_desktop(self, true);
1048    }
1049
1050    fn unshow_desktop_request(&mut self, _xwm: XwmId) {
1051        wm::show_desktop(self, false);
1052    }
1053
1054    fn disconnected(&mut self, _xwm: XwmId) {
1055        eprintln!("[Wayland] Xwayland disconnected; starting it again in {RESTART_DELAY:?}.");
1056        let ids: Vec<WindowId> = self.xwm.windows.keys().copied().collect();
1057        for id in ids {
1058            if let Some(window) = self.xwm.windows.remove(&id) {
1059                self.space.unmap_elem(&window);
1060            }
1061            wm::window_removed(self, id);
1062            crate::wayland::foreign_toplevel::remove(self, id);
1063        }
1064        self.xwm.wm = None;
1065        self.xwm.display = None;
1066        self.xwm.extras = None;
1067        self.xwm.restart_at = Some(Instant::now() + RESTART_DELAY);
1068    }
1069}
1070
1071#[cfg(test)]
1072mod tests {
1073    use super::*;
1074
1075    #[test]
1076    fn a_named_xwayland_is_found_first_and_a_missing_one_is_not_named() {
1077        let dir = std::env::temp_dir().join(format!("pixelflux-xwayland-{}", std::process::id()));
1078        std::fs::create_dir_all(&dir).unwrap();
1079        let bin = dir.join("Xwayland");
1080        std::fs::write(&bin, "").unwrap();
1081        assert_eq!(
1082            xwayland_path(Some(bin.clone().into()), Some("/usr/bin:/bin".into())),
1083            Some(format!("{}:/usr/bin:/bin", dir.display()))
1084        );
1085        assert_eq!(
1086            xwayland_path(Some(bin.into()), None),
1087            Some(dir.display().to_string())
1088        );
1089        assert_eq!(
1090            xwayland_path(Some(dir.join("missing").into()), Some("/usr/bin".into())),
1091            None
1092        );
1093        assert_eq!(xwayland_path(None, Some("/usr/bin".into())), None);
1094        std::fs::remove_dir_all(&dir).unwrap();
1095    }
1096}