tauri_plugin_widgets/linux/
mod.rs1use tauri::{Runtime, WebviewWindow};
4use x11rb::connection::Connection;
5use x11rb::protocol::xproto::{AtomEnum, ConnectionExt as XprotoConnectionExt, PropMode};
6use x11rb::wrapper::ConnectionExt as WrapperConnectionExt;
7
8pub fn pin_widget_window<R: Runtime>(win: &WebviewWindow<R>, skip_taskbar: bool) {
14 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 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], );
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 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 #[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}