test again

This commit is contained in:
Haapy
2026-05-14 23:26:02 +00:00
parent b496914b3c
commit 6a4f4b4aa9
8 changed files with 160 additions and 96 deletions

View File

@@ -7,28 +7,27 @@ use x11rb::atom_manager;
use x11rb::connection::Connection;
use x11rb::protocol::xproto::{
AtomEnum, ChangeWindowAttributesAux, ClientMessageEvent, ConfigureWindowAux,
ConnectionExt as XprotoConnectionExt, EventMask, PropMode,
ConnectionExt as XprotoConnectionExt, EventMask, StackMode,
};
use x11rb::protocol::Event;
use x11rb::rust_connection::RustConnection;
use x11rb::wrapper::ConnectionExt as WrapperConnectionExt;
atom_manager! {
pub Atoms: AtomsCookie {
_NET_CLIENT_LIST,
_NET_WM_PID,
_NET_WM_NAME,
_NET_WM_STATE,
_NET_WM_STATE_ABOVE,
_NET_WM_STATE_SKIP_TASKBAR,
_NET_WM_STATE_SKIP_PAGER,
_MOTIF_WM_HINTS,
_XEMBED,
_XEMBED_INFO,
WM_PROTOCOLS,
WM_DELETE_WINDOW,
UTF8_STRING,
}
}
// XEmbed protocol message opcodes (subset).
const XEMBED_EMBEDDED_NOTIFY: u32 = 0;
#[derive(Clone)]
struct X11Conn {
conn: Arc<RustConnection>,
@@ -46,6 +45,7 @@ pub struct AppInfo {
pub struct X11State {
conn: Mutex<Option<X11Conn>>,
tauri_xid: Mutex<Option<u32>>,
windows: Arc<Mutex<HashMap<u32, AppInfo>>>,
}
@@ -53,11 +53,12 @@ impl X11State {
pub fn new() -> Self {
Self {
conn: Mutex::new(None),
tauri_xid: Mutex::new(None),
windows: Arc::new(Mutex::new(HashMap::new())),
}
}
fn ensure(&self, app: &AppHandle) -> Result<X11Conn, String> {
fn ensure_conn(&self, app: &AppHandle) -> Result<X11Conn, String> {
let mut guard = self.conn.lock().unwrap();
if let Some(c) = guard.as_ref() {
return Ok(c.clone());
@@ -70,15 +71,6 @@ impl X11State {
.map_err(|e| e.to_string())?;
let root = conn.setup().roots[screen_num].root;
// Subscribe to destroy notifications for embedded windows.
conn.change_window_attributes(
root,
&ChangeWindowAttributesAux::new().event_mask(EventMask::SUBSTRUCTURE_NOTIFY),
)
.map_err(|e| e.to_string())?;
conn.flush().map_err(|e| e.to_string())?;
// Background event thread for DestroyNotify.
let conn_t = conn.clone();
let windows_t = self.windows.clone();
let app_t = app.clone();
@@ -88,6 +80,39 @@ impl X11State {
*guard = Some(c.clone());
Ok(c)
}
fn ensure_xid(
&self,
app: &AppHandle,
window: &tauri::WebviewWindow,
) -> Result<u32, String> {
let mut guard = self.tauri_xid.lock().unwrap();
if let Some(xid) = *guard {
return Ok(xid);
}
let xid = fetch_tauri_xid(app, window.clone())?;
*guard = Some(xid);
Ok(xid)
}
}
fn fetch_tauri_xid(app: &AppHandle, window: tauri::WebviewWindow) -> Result<u32, String> {
use std::sync::mpsc;
let (tx, rx) = mpsc::sync_channel(1);
app.run_on_main_thread(move || {
let result: Result<u32, String> = (|| {
use gtk::prelude::*;
let gtk_win = window.gtk_window().map_err(|e| e.to_string())?;
let gdk_win = gtk_win.window().ok_or_else(|| "no GDK window yet".to_string())?;
let x11: gdkx11::X11Window = gdk_win
.downcast()
.map_err(|_| "GDK window is not an X11Window".to_string())?;
Ok(x11.xid() as u32)
})();
let _ = tx.send(result);
})
.map_err(|e| e.to_string())?;
rx.recv().map_err(|e| e.to_string())?
}
fn event_loop(
@@ -124,9 +149,11 @@ pub struct LaunchedApp {
pub async fn app_launch(
app: AppHandle,
state: State<'_, X11State>,
window: tauri::WebviewWindow,
opts: LaunchOpts,
) -> Result<LaunchedApp, String> {
let c = state.ensure(&app)?;
let c = state.ensure_conn(&app)?;
let parent_xid = state.ensure_xid(&app, &window)?;
let parts = shell_words::split(&opts.command).map_err(|e| e.to_string())?;
if parts.is_empty() {
@@ -139,26 +166,26 @@ pub async fn app_launch(
.spawn()
.map_err(|e| format!("spawn failed: {}", e))?;
let root_pid = child.id();
// We don't wait()—the child lives independently. Tauri will SIGHUP it on exit.
// Detach: we don't wait() — Tauri exiting will SIGHUP it via the X11 parent.
std::mem::forget(child);
let xid = wait_for_window_by_pid(&c, root_pid, Duration::from_secs(20))
let client_xid = wait_for_window_by_pid(&c, root_pid, Duration::from_secs(20))
.ok_or_else(|| "could not find window for launched process".to_string())?;
set_embedded_window_props(&c, xid)?;
embed_window(&c, client_xid, parent_xid)?;
let title = read_window_title(&c, xid).unwrap_or_else(|| opts.command.clone());
let title = read_window_title(&c, client_xid).unwrap_or_else(|| opts.command.clone());
state.windows.lock().unwrap().insert(
xid,
client_xid,
AppInfo {
xid,
xid: client_xid,
pid: root_pid,
command: opts.command.clone(),
},
);
Ok(LaunchedApp { xid, title })
Ok(LaunchedApp { xid: client_xid, title })
}
#[tauri::command]
@@ -172,22 +199,25 @@ pub fn app_set_geometry(
width: f64,
height: f64,
) -> Result<(), String> {
let c = state.ensure(&app)?;
let c = state.ensure_conn(&app)?;
let sf = window.scale_factor().map_err(|e| e.to_string())?;
let pos = window.inner_position().map_err(|e| e.to_string())?;
let root_x = pos.x + (x * sf).round() as i32;
let root_y = pos.y + (y * sf).round() as i32;
let w = ((width * sf).round() as i32).max(1) as u32;
let h = ((height * sf).round() as i32).max(1) as u32;
let outer = window.outer_position().map_err(|e| e.to_string())?;
let inner = window.inner_position().map_err(|e| e.to_string())?;
let off_x = inner.x - outer.x;
let off_y = inner.y - outer.y;
let phys_x = (x * sf).round() as i32 + off_x;
let phys_y = (y * sf).round() as i32 + off_y;
let phys_w = ((width * sf).round() as i32).max(1) as u32;
let phys_h = ((height * sf).round() as i32).max(1) as u32;
c.conn
.configure_window(
xid,
&ConfigureWindowAux::new()
.x(root_x)
.y(root_y)
.width(w)
.height(h)
.stack_mode(x11rb::protocol::xproto::StackMode::ABOVE),
.x(phys_x)
.y(phys_y)
.width(phys_w)
.height(phys_h)
.stack_mode(StackMode::ABOVE),
)
.map_err(|e| e.to_string())?;
c.conn.flush().map_err(|e| e.to_string())?;
@@ -201,7 +231,7 @@ pub fn app_set_visible(
xid: u32,
visible: bool,
) -> Result<(), String> {
let c = state.ensure(&app)?;
let c = state.ensure_conn(&app)?;
if visible {
c.conn.map_window(xid).map_err(|e| e.to_string())?;
} else {
@@ -211,10 +241,28 @@ pub fn app_set_visible(
Ok(())
}
#[tauri::command]
pub fn app_set_all_visible(
app: AppHandle,
state: State<'_, X11State>,
visible: bool,
) -> Result<(), String> {
let c = state.ensure_conn(&app)?;
let xids: Vec<u32> = state.windows.lock().unwrap().keys().copied().collect();
for xid in xids {
if visible {
let _ = c.conn.map_window(xid);
} else {
let _ = c.conn.unmap_window(xid);
}
}
let _ = c.conn.flush();
Ok(())
}
#[tauri::command]
pub fn app_close(app: AppHandle, state: State<'_, X11State>, xid: u32) -> Result<(), String> {
let c = state.ensure(&app)?;
// Send WM_DELETE_WINDOW to politely ask the app to close.
let c = state.ensure_conn(&app)?;
let event = ClientMessageEvent::new(
32,
xid,
@@ -227,6 +275,46 @@ pub fn app_close(app: AppHandle, state: State<'_, X11State>, xid: u32) -> Result
Ok(())
}
fn embed_window(c: &X11Conn, client_xid: u32, parent_xid: u32) -> Result<(), String> {
// Listen for destroy / property changes on the embedded window.
c.conn
.change_window_attributes(
client_xid,
&ChangeWindowAttributesAux::new()
.event_mask(EventMask::STRUCTURE_NOTIFY | EventMask::PROPERTY_CHANGE),
)
.map_err(|e| e.to_string())?;
// Reparent into Tauri's X11 toplevel.
c.conn
.reparent_window(client_xid, parent_xid, 0, 0)
.map_err(|e| e.to_string())?;
c.conn.map_window(client_xid).map_err(|e| e.to_string())?;
// Stack above the webview's GDK X11 surface (siblings under Tauri toplevel).
c.conn
.configure_window(
client_xid,
&ConfigureWindowAux::new().stack_mode(StackMode::ABOVE),
)
.map_err(|e| e.to_string())?;
c.conn.flush().map_err(|e| e.to_string())?;
// Inform the client it is now embedded (XEmbed protocol).
let event = ClientMessageEvent::new(
32,
client_xid,
c.atoms._XEMBED,
[x11rb::CURRENT_TIME, XEMBED_EMBEDDED_NOTIFY, 0, parent_xid, 0],
);
let _ = c.conn.send_event(false, client_xid, EventMask::NO_EVENT, event);
c.conn.flush().map_err(|e| e.to_string())?;
Ok(())
}
fn wait_for_window_by_pid(c: &X11Conn, root_pid: u32, timeout: Duration) -> Option<u32> {
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
@@ -240,7 +328,6 @@ fn wait_for_window_by_pid(c: &X11Conn, root_pid: u32, timeout: Duration) -> Opti
}
fn collect_descendant_pids(root: u32) -> Vec<u32> {
// Walk /proc to find all processes whose ancestor chain includes root.
let mut parents: HashMap<u32, u32> = HashMap::new();
let Ok(entries) = std::fs::read_dir("/proc") else {
return vec![root];
@@ -257,7 +344,6 @@ fn collect_descendant_pids(root: u32) -> Vec<u32> {
let Ok(stat) = std::fs::read_to_string(&stat_path) else {
continue;
};
// Format: "pid (comm) state ppid ..."
if let Some(rparen) = stat.rfind(')') {
let rest = &stat[rparen + 1..];
let fields: Vec<&str> = rest.split_whitespace().collect();
@@ -319,7 +405,6 @@ fn read_window_title(c: &X11Conn, xid: u32) -> Option<String> {
.ok()?;
let s = String::from_utf8_lossy(&reply.value).to_string();
if s.is_empty() {
// fallback to WM_NAME
let r2 = c
.conn
.get_property(false, xid, AtomEnum::WM_NAME, AtomEnum::STRING, 0, 1024)
@@ -331,40 +416,3 @@ fn read_window_title(c: &X11Conn, xid: u32) -> Option<String> {
Some(s)
}
}
fn set_embedded_window_props(c: &X11Conn, xid: u32) -> Result<(), String> {
// _MOTIF_WM_HINTS — flags=DECORATIONS (1<<1), decorations=0 (none).
let motif = [2u32, 0, 0, 0, 0];
c.conn
.change_property32(
PropMode::REPLACE,
xid,
c.atoms._MOTIF_WM_HINTS,
c.atoms._MOTIF_WM_HINTS,
&motif,
)
.map_err(|e| e.to_string())?;
// _NET_WM_STATE_ADD: ABOVE + SKIP_TASKBAR + SKIP_PAGER.
for state_atom in [
c.atoms._NET_WM_STATE_ABOVE,
c.atoms._NET_WM_STATE_SKIP_TASKBAR,
c.atoms._NET_WM_STATE_SKIP_PAGER,
] {
let event = ClientMessageEvent::new(
32,
xid,
c.atoms._NET_WM_STATE,
[1, state_atom, 0, 0, 0], // _NET_WM_STATE_ADD = 1
);
let _ = c.conn.send_event(
false,
c.root,
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
event,
);
}
c.conn.flush().map_err(|e| e.to_string())?;
Ok(())
}