Skip to main content

tauri_plugin_widgets/linux/
mod.rs

1//! Linux desktop-widget pinning: X11 `_NET_WM_*` hints and optional gtk-layer-shell.
2
3use tauri::{Runtime, WebviewWindow};
4use x11rb::connection::Connection;
5use x11rb::protocol::xproto::{AtomEnum, ConnectionExt as XprotoConnectionExt, PropMode};
6use x11rb::wrapper::ConnectionExt as WrapperConnectionExt;
7
8/// Pin a widget webview as a desktop-layer surface when possible.
9///
10/// - **X11**: `_NET_WM_WINDOW_TYPE_DESKTOP` (+ `_NET_WM_STATE_SKIP_TASKBAR` when requested).
11/// - **Wayland** (`feature = "layer-shell"`): remap into a gtk-layer-shell Background surface.
12/// - Failures are logged; the ordinary frameless window remains usable (fallback).
13pub fn pin_widget_window<R: Runtime>(win: &WebviewWindow<R>, skip_taskbar: bool) {
14    // Prefer the actual native backend over env vars: X11 apps can still run
15    // when WAYLAND_DISPLAY is set (XWayland). Layer-shell only when the handle
16    // is not X11 (covers compositors that omit WAYLAND_DISPLAY for wayland-0).
17    let is_x11 = x11_xid(win).is_some();
18
19    #[cfg(feature = "layer-shell")]
20    {
21        if !is_x11 {
22            match apply_layer_shell(win) {
23                Ok(()) => {
24                    log::debug!("linux: gtk-layer-shell Background applied");
25                    return;
26                }
27                Err(e) => {
28                    log::warn!("linux: layer-shell failed ({e}); keeping normal window");
29                }
30            }
31        }
32    }
33
34    if is_x11 {
35        if let Err(e) = apply_x11_desktop_hints(win, skip_taskbar) {
36            log::warn!("linux: X11 desktop hints failed: {e}");
37        }
38    }
39}
40
41fn apply_x11_desktop_hints<R: Runtime>(
42    win: &WebviewWindow<R>,
43    skip_taskbar: bool,
44) -> Result<(), String> {
45    let xid = x11_xid(win).ok_or_else(|| "no X11 window id".to_string())?;
46
47    let (conn, _) = x11rb::connect(None).map_err(|e| format!("x11 connect: {e}"))?;
48
49    let type_atom = intern(&conn, b"_NET_WM_WINDOW_TYPE")?;
50    let desktop_atom = intern(&conn, b"_NET_WM_WINDOW_TYPE_DESKTOP")?;
51    WrapperConnectionExt::change_property32(
52        &conn,
53        PropMode::REPLACE,
54        xid,
55        type_atom,
56        AtomEnum::ATOM,
57        &[desktop_atom],
58    )
59    .map_err(|e| format!("change_property TYPE: {e}"))?;
60
61    if skip_taskbar {
62        let state_atom = intern(&conn, b"_NET_WM_STATE")?;
63        let skip_atom = intern(&conn, b"_NET_WM_STATE_SKIP_TASKBAR")?;
64        // EWMH ADD via ClientMessage — do not REPLACE _NET_WM_STATE (wipes ABOVE / sticky).
65        let root = conn.setup().roots.first().map(|s| s.root).unwrap_or(0);
66        let event = x11rb::protocol::xproto::ClientMessageEvent::new(
67            32,
68            xid,
69            state_atom,
70            [1u32, skip_atom, 0, 0, 0], // 1 = _NET_WM_STATE_ADD
71        );
72        conn.send_event(
73            false,
74            root,
75            x11rb::protocol::xproto::EventMask::SUBSTRUCTURE_REDIRECT
76                | x11rb::protocol::xproto::EventMask::SUBSTRUCTURE_NOTIFY,
77            event,
78        )
79        .map_err(|e| format!("send_event STATE ADD: {e}"))?;
80    }
81
82    conn.flush().map_err(|e| format!("x11 flush: {e}"))?;
83    log::debug!("linux: X11 DESKTOP hints set on xid={xid}");
84    Ok(())
85}
86
87fn x11_xid<R: Runtime>(win: &WebviewWindow<R>) -> Option<u32> {
88    use raw_window_handle::{HasWindowHandle, RawWindowHandle};
89
90    let handle = win.window_handle().ok()?;
91    match handle.as_raw() {
92        RawWindowHandle::Xlib(h) => Some(h.window as u32),
93        RawWindowHandle::Xcb(h) => Some(h.window.get()),
94        _ => None,
95    }
96}
97
98fn intern(conn: &impl Connection, name: &[u8]) -> Result<u32, String> {
99    Ok(XprotoConnectionExt::intern_atom(conn, false, name)
100        .map_err(|e| format!("intern_atom: {e}"))?
101        .reply()
102        .map_err(|e| format!("intern_atom reply: {e}"))?
103        .atom)
104}
105
106#[cfg(feature = "layer-shell")]
107fn apply_layer_shell<R: Runtime>(win: &WebviewWindow<R>) -> Result<(), String> {
108    use gtk::prelude::*;
109    use gtk_layer_shell::{Edge, Layer, LayerShell};
110
111    // Window must not be mapped when init_layer_shell runs — hide first.
112    let _ = win.hide();
113
114    let old = win.gtk_window().map_err(|e| format!("gtk_window: {e}"))?;
115    let app = old
116        .application()
117        .ok_or_else(|| "gtk window has no application".to_string())?;
118
119    let vbox = win
120        .default_vbox()
121        .map_err(|e| format!("default_vbox: {e}"))?;
122    old.remove(&vbox);
123
124    let layer_win = gtk::ApplicationWindow::new(&app);
125    layer_win.set_app_paintable(true);
126    layer_win.add(&vbox);
127
128    layer_win.init_layer_shell();
129    layer_win.set_layer(Layer::Background);
130    layer_win.set_anchor(Edge::Left, false);
131    layer_win.set_anchor(Edge::Right, false);
132    layer_win.set_anchor(Edge::Top, false);
133    layer_win.set_anchor(Edge::Bottom, false);
134    // Keyboard interactivity — best-effort across gtk-layer-shell versions.
135    #[allow(unused_must_use)]
136    {
137        layer_win.set_keyboard_interactivity(false);
138    }
139
140    let (w, h) = win
141        .inner_size()
142        .map(|s| (s.width as i32, s.height as i32))
143        .unwrap_or((340, 340));
144    layer_win.set_default_size(w, h);
145    layer_win.show_all();
146
147    Ok(())
148}