Skip to main content

tauri_plugin_widgets/linux/
mod.rs

1//! Linux desktop-widget pinning: GDK/EWMH hints before the first map and optional
2//! gtk-layer-shell on Wayland.
3
4use tauri::{Runtime, WebviewWindow};
5
6use crate::desktop::placement::WindowModes;
7use crate::models::{WidgetWindowConfig, WindowLayer};
8
9/// A native handle query; `Err` when it fails or panics.
10pub(crate) fn native<T, E: std::fmt::Display>(
11    what: &str,
12    f: impl FnOnce() -> Result<T, E>,
13) -> Result<T, String> {
14    // ponytail: MockRuntime's `gtk_window` / `default_vbox` are `unimplemented!()` (as its
15    // monitor queries, see `window::monitor_query`); without the catch a widget or activity
16    // command panics and never answers under `tauri::test`.
17    std::panic::catch_unwind(std::panic::AssertUnwindSafe(f))
18        .map_err(|_| format!("{what}: not available"))?
19        .map_err(|e| format!("{what}: {e}"))
20}
21
22/// Pin a **not yet shown** widget window.
23///
24/// - **Wayland** (`feature = "layer-shell"`, layer other than `normal`): the webview moves
25///   into a gtk-layer-shell surface (anchors / margins from the position, monitor,
26///   namespace = label) which is shown here. Returns `true`.
27/// - Otherwise sets GDK hints that land in the first map, so the window manager never
28///   sees an ordinary window: `_NET_WM_WINDOW_TYPE_DESKTOP` for the `desktop` layer,
29///   `SKIP_TASKBAR` / `SKIP_PAGER`, `STICKY`, `BELOW`. Returns `false`: the caller shows
30///   the window and re-applies its position (WMs often ignore it for DESKTOP windows).
31///
32/// Failures are logged; the ordinary frameless window remains usable.
33pub(crate) fn pin_widget_window<R: Runtime>(
34    win: &WebviewWindow<R>,
35    cfg: &WidgetWindowConfig,
36    modes: &WindowModes,
37    pos: Option<(f64, f64)>,
38) -> bool {
39    // Prefer the actual native backend over env vars: X11 apps can still run when
40    // WAYLAND_DISPLAY is set (XWayland).
41    let is_x11 = x11_xid(win).is_some();
42
43    #[cfg(feature = "layer-shell")]
44    if !is_x11 && modes.layer != WindowLayer::Normal {
45        match apply_layer_shell(win, cfg, modes, pos) {
46            Ok(()) => {
47                log::debug!("linux: gtk-layer-shell surface for '{}'", cfg.label);
48                return true;
49            }
50            Err(e) => log::warn!("linux: layer-shell failed ({e}); keeping normal window"),
51        }
52    }
53    #[cfg(not(feature = "layer-shell"))]
54    let _ = (pos, is_x11);
55
56    let island = cfg.label == crate::desktop::island::LABEL;
57    if let Err(e) = apply_gtk_hints(win, modes, cfg.skip_taskbar, island) {
58        log::warn!("linux: desktop hints failed: {e}");
59    }
60    false
61}
62
63fn apply_gtk_hints<R: Runtime>(
64    win: &WebviewWindow<R>,
65    modes: &WindowModes,
66    skip_taskbar: bool,
67    island: bool,
68) -> Result<(), String> {
69    use gtk::prelude::*;
70    let gw = native("gtk_window", || win.gtk_window())?;
71    if modes.layer == WindowLayer::Desktop {
72        gw.set_type_hint(gtk::gdk::WindowTypeHint::Desktop);
73    }
74    // Activity island: a DOCK stays above normal windows on every desktop, unfocused.
75    if island {
76        gw.set_type_hint(gtk::gdk::WindowTypeHint::Dock);
77    }
78    if skip_taskbar {
79        gw.set_skip_taskbar_hint(true);
80        gw.set_skip_pager_hint(true);
81    }
82    if modes.all_workspaces {
83        gw.stick();
84    }
85    gw.set_keep_below(modes.on_bottom());
86    gw.set_keep_above(modes.on_top());
87    Ok(())
88}
89
90fn x11_xid<R: Runtime>(win: &WebviewWindow<R>) -> Option<u32> {
91    use raw_window_handle::{HasWindowHandle, RawWindowHandle};
92
93    let handle = win.window_handle().ok()?;
94    match handle.as_raw() {
95        RawWindowHandle::Xlib(h) => Some(h.window as u32),
96        RawWindowHandle::Xcb(h) => Some(h.window.get()),
97        _ => None,
98    }
99}
100
101#[cfg(feature = "layer-shell")]
102fn apply_layer_shell<R: Runtime>(
103    win: &WebviewWindow<R>,
104    cfg: &WidgetWindowConfig,
105    modes: &WindowModes,
106    pos: Option<(f64, f64)>,
107) -> Result<(), String> {
108    use crate::desktop::placement::{layer_anchor, Rect};
109    use gtk::prelude::*;
110    use gtk_layer_shell::{Edge, Layer, LayerShell};
111
112    let island = cfg.label == crate::desktop::island::LABEL;
113    let layer = match modes.layer {
114        _ if island => Layer::Overlay,
115        WindowLayer::Desktop => Layer::Background,
116        WindowLayer::Bottom => Layer::Bottom,
117        WindowLayer::Top => Layer::Top,
118        WindowLayer::Normal => return Err("normal layer".into()),
119    };
120
121    // The Tauri window is still unmapped: init_layer_shell needs that.
122    let old = native("gtk_window", || win.gtk_window())?;
123    let app = old
124        .application()
125        .ok_or_else(|| "gtk window has no application".to_string())?;
126    let vbox = native("default_vbox", || win.default_vbox())?;
127    old.remove(&vbox);
128
129    let layer_win = gtk::ApplicationWindow::new(&app);
130    layer_win.set_app_paintable(true);
131    if let Some(v) = WidgetExt::screen(&layer_win).and_then(|s| s.rgba_visual()) {
132        layer_win.set_visual(Some(&v));
133    }
134    layer_win.add(&vbox);
135
136    layer_win.init_layer_shell();
137    layer_win.set_layer(layer);
138    layer_win.set_namespace(&cfg.label);
139
140    let monitor = gtk::gdk::Display::default().and_then(|d| match pos {
141        Some((x, y)) => d.monitor_at_point(x as i32, y as i32),
142        None => d.primary_monitor(),
143    });
144    let geom = monitor.as_ref().map_or(Rect::default(), |m| {
145        let g = m.geometry();
146        Rect {
147            x: g.x() as f64,
148            y: g.y() as f64,
149            w: g.width() as f64,
150            h: g.height() as f64,
151        }
152    });
153    if let Some(m) = &monitor {
154        layer_win.set_monitor(m);
155    }
156    let mut a = layer_anchor(cfg.placement.as_ref(), pos, geom);
157    // Activity island: top edge only, so the compositor keeps it centred as it resizes.
158    if island {
159        a.left = None;
160        a.right = None;
161    }
162    for (edge, margin) in [
163        (Edge::Top, a.top),
164        (Edge::Bottom, a.bottom),
165        (Edge::Left, a.left),
166        (Edge::Right, a.right),
167    ] {
168        layer_win.set_anchor(edge, margin.is_some());
169        layer_win.set_layer_shell_margin(edge, margin.unwrap_or(0));
170    }
171    #[allow(deprecated)]
172    layer_win.set_keyboard_interactivity(modes.focusable);
173    if modes.click_through {
174        layer_win.input_shape_combine_region(Some(&gtk::cairo::Region::create()));
175    }
176
177    layer_win.set_default_size(cfg.width.round() as i32, cfg.height.round() as i32);
178    layer_win.show_all();
179
180    // The webview now lives in `layer_win`; close it together with the (hidden) Tauri
181    // window, otherwise close_widget_window leaves it on screen.
182    let layer = layer_win.clone();
183    old.connect_destroy(move |_| layer.close());
184
185    Ok(())
186}