test again
This commit is contained in:
File diff suppressed because one or more lines are too long
2
dist/index.html
vendored
2
dist/index.html
vendored
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Infinite</title>
|
<title>Infinite</title>
|
||||||
<script type="module" crossorigin src="/assets/index-D8BvfjIZ.js"></script>
|
<script type="module" crossorigin src="/assets/index-B-HByrTS.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-fBlcXAv5.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-fBlcXAv5.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -80,6 +80,8 @@ name = "app"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
"gdkx11",
|
||||||
|
"gtk",
|
||||||
"log",
|
"log",
|
||||||
"portable-pty",
|
"portable-pty",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -28,3 +28,7 @@ uuid = { version = "1", features = ["v4"] }
|
|||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
x11rb = "0.13"
|
x11rb = "0.13"
|
||||||
shell-words = "1.1"
|
shell-words = "1.1"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
gtk = "0.18"
|
||||||
|
gdkx11 = "0.18"
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ mod pty;
|
|||||||
mod x11mod;
|
mod x11mod;
|
||||||
|
|
||||||
use pty::{pty_kill, pty_resize, pty_spawn, pty_write, PtyState};
|
use pty::{pty_kill, pty_resize, pty_spawn, pty_write, PtyState};
|
||||||
use x11mod::{app_close, app_launch, app_set_geometry, app_set_visible, X11State};
|
use x11mod::{
|
||||||
|
app_close, app_launch, app_set_all_visible, app_set_geometry, app_set_visible, X11State,
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
@@ -27,6 +29,7 @@ pub fn run() {
|
|||||||
app_launch,
|
app_launch,
|
||||||
app_set_geometry,
|
app_set_geometry,
|
||||||
app_set_visible,
|
app_set_visible,
|
||||||
|
app_set_all_visible,
|
||||||
app_close,
|
app_close,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
|
|||||||
@@ -7,28 +7,27 @@ use x11rb::atom_manager;
|
|||||||
use x11rb::connection::Connection;
|
use x11rb::connection::Connection;
|
||||||
use x11rb::protocol::xproto::{
|
use x11rb::protocol::xproto::{
|
||||||
AtomEnum, ChangeWindowAttributesAux, ClientMessageEvent, ConfigureWindowAux,
|
AtomEnum, ChangeWindowAttributesAux, ClientMessageEvent, ConfigureWindowAux,
|
||||||
ConnectionExt as XprotoConnectionExt, EventMask, PropMode,
|
ConnectionExt as XprotoConnectionExt, EventMask, StackMode,
|
||||||
};
|
};
|
||||||
use x11rb::protocol::Event;
|
use x11rb::protocol::Event;
|
||||||
use x11rb::rust_connection::RustConnection;
|
use x11rb::rust_connection::RustConnection;
|
||||||
use x11rb::wrapper::ConnectionExt as WrapperConnectionExt;
|
|
||||||
|
|
||||||
atom_manager! {
|
atom_manager! {
|
||||||
pub Atoms: AtomsCookie {
|
pub Atoms: AtomsCookie {
|
||||||
_NET_CLIENT_LIST,
|
_NET_CLIENT_LIST,
|
||||||
_NET_WM_PID,
|
_NET_WM_PID,
|
||||||
_NET_WM_NAME,
|
_NET_WM_NAME,
|
||||||
_NET_WM_STATE,
|
_XEMBED,
|
||||||
_NET_WM_STATE_ABOVE,
|
_XEMBED_INFO,
|
||||||
_NET_WM_STATE_SKIP_TASKBAR,
|
|
||||||
_NET_WM_STATE_SKIP_PAGER,
|
|
||||||
_MOTIF_WM_HINTS,
|
|
||||||
WM_PROTOCOLS,
|
WM_PROTOCOLS,
|
||||||
WM_DELETE_WINDOW,
|
WM_DELETE_WINDOW,
|
||||||
UTF8_STRING,
|
UTF8_STRING,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XEmbed protocol message opcodes (subset).
|
||||||
|
const XEMBED_EMBEDDED_NOTIFY: u32 = 0;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct X11Conn {
|
struct X11Conn {
|
||||||
conn: Arc<RustConnection>,
|
conn: Arc<RustConnection>,
|
||||||
@@ -46,6 +45,7 @@ pub struct AppInfo {
|
|||||||
|
|
||||||
pub struct X11State {
|
pub struct X11State {
|
||||||
conn: Mutex<Option<X11Conn>>,
|
conn: Mutex<Option<X11Conn>>,
|
||||||
|
tauri_xid: Mutex<Option<u32>>,
|
||||||
windows: Arc<Mutex<HashMap<u32, AppInfo>>>,
|
windows: Arc<Mutex<HashMap<u32, AppInfo>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,11 +53,12 @@ impl X11State {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
conn: Mutex::new(None),
|
conn: Mutex::new(None),
|
||||||
|
tauri_xid: Mutex::new(None),
|
||||||
windows: Arc::new(Mutex::new(HashMap::new())),
|
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();
|
let mut guard = self.conn.lock().unwrap();
|
||||||
if let Some(c) = guard.as_ref() {
|
if let Some(c) = guard.as_ref() {
|
||||||
return Ok(c.clone());
|
return Ok(c.clone());
|
||||||
@@ -70,15 +71,6 @@ impl X11State {
|
|||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
let root = conn.setup().roots[screen_num].root;
|
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 conn_t = conn.clone();
|
||||||
let windows_t = self.windows.clone();
|
let windows_t = self.windows.clone();
|
||||||
let app_t = app.clone();
|
let app_t = app.clone();
|
||||||
@@ -88,6 +80,39 @@ impl X11State {
|
|||||||
*guard = Some(c.clone());
|
*guard = Some(c.clone());
|
||||||
Ok(c)
|
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(
|
fn event_loop(
|
||||||
@@ -124,9 +149,11 @@ pub struct LaunchedApp {
|
|||||||
pub async fn app_launch(
|
pub async fn app_launch(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
state: State<'_, X11State>,
|
state: State<'_, X11State>,
|
||||||
|
window: tauri::WebviewWindow,
|
||||||
opts: LaunchOpts,
|
opts: LaunchOpts,
|
||||||
) -> Result<LaunchedApp, String> {
|
) -> 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())?;
|
let parts = shell_words::split(&opts.command).map_err(|e| e.to_string())?;
|
||||||
if parts.is_empty() {
|
if parts.is_empty() {
|
||||||
@@ -139,26 +166,26 @@ pub async fn app_launch(
|
|||||||
.spawn()
|
.spawn()
|
||||||
.map_err(|e| format!("spawn failed: {}", e))?;
|
.map_err(|e| format!("spawn failed: {}", e))?;
|
||||||
let root_pid = child.id();
|
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);
|
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())?;
|
.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(
|
state.windows.lock().unwrap().insert(
|
||||||
xid,
|
client_xid,
|
||||||
AppInfo {
|
AppInfo {
|
||||||
xid,
|
xid: client_xid,
|
||||||
pid: root_pid,
|
pid: root_pid,
|
||||||
command: opts.command.clone(),
|
command: opts.command.clone(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(LaunchedApp { xid, title })
|
Ok(LaunchedApp { xid: client_xid, title })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -172,22 +199,25 @@ pub fn app_set_geometry(
|
|||||||
width: f64,
|
width: f64,
|
||||||
height: f64,
|
height: f64,
|
||||||
) -> Result<(), String> {
|
) -> 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 sf = window.scale_factor().map_err(|e| e.to_string())?;
|
||||||
let pos = window.inner_position().map_err(|e| e.to_string())?;
|
let outer = window.outer_position().map_err(|e| e.to_string())?;
|
||||||
let root_x = pos.x + (x * sf).round() as i32;
|
let inner = window.inner_position().map_err(|e| e.to_string())?;
|
||||||
let root_y = pos.y + (y * sf).round() as i32;
|
let off_x = inner.x - outer.x;
|
||||||
let w = ((width * sf).round() as i32).max(1) as u32;
|
let off_y = inner.y - outer.y;
|
||||||
let h = ((height * sf).round() as i32).max(1) as u32;
|
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
|
c.conn
|
||||||
.configure_window(
|
.configure_window(
|
||||||
xid,
|
xid,
|
||||||
&ConfigureWindowAux::new()
|
&ConfigureWindowAux::new()
|
||||||
.x(root_x)
|
.x(phys_x)
|
||||||
.y(root_y)
|
.y(phys_y)
|
||||||
.width(w)
|
.width(phys_w)
|
||||||
.height(h)
|
.height(phys_h)
|
||||||
.stack_mode(x11rb::protocol::xproto::StackMode::ABOVE),
|
.stack_mode(StackMode::ABOVE),
|
||||||
)
|
)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
c.conn.flush().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,
|
xid: u32,
|
||||||
visible: bool,
|
visible: bool,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let c = state.ensure(&app)?;
|
let c = state.ensure_conn(&app)?;
|
||||||
if visible {
|
if visible {
|
||||||
c.conn.map_window(xid).map_err(|e| e.to_string())?;
|
c.conn.map_window(xid).map_err(|e| e.to_string())?;
|
||||||
} else {
|
} else {
|
||||||
@@ -211,10 +241,28 @@ pub fn app_set_visible(
|
|||||||
Ok(())
|
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]
|
#[tauri::command]
|
||||||
pub fn app_close(app: AppHandle, state: State<'_, X11State>, xid: u32) -> Result<(), String> {
|
pub fn app_close(app: AppHandle, state: State<'_, X11State>, xid: u32) -> Result<(), String> {
|
||||||
let c = state.ensure(&app)?;
|
let c = state.ensure_conn(&app)?;
|
||||||
// Send WM_DELETE_WINDOW to politely ask the app to close.
|
|
||||||
let event = ClientMessageEvent::new(
|
let event = ClientMessageEvent::new(
|
||||||
32,
|
32,
|
||||||
xid,
|
xid,
|
||||||
@@ -227,6 +275,46 @@ pub fn app_close(app: AppHandle, state: State<'_, X11State>, xid: u32) -> Result
|
|||||||
Ok(())
|
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> {
|
fn wait_for_window_by_pid(c: &X11Conn, root_pid: u32, timeout: Duration) -> Option<u32> {
|
||||||
let deadline = Instant::now() + timeout;
|
let deadline = Instant::now() + timeout;
|
||||||
while Instant::now() < deadline {
|
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> {
|
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 mut parents: HashMap<u32, u32> = HashMap::new();
|
||||||
let Ok(entries) = std::fs::read_dir("/proc") else {
|
let Ok(entries) = std::fs::read_dir("/proc") else {
|
||||||
return vec![root];
|
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 {
|
let Ok(stat) = std::fs::read_to_string(&stat_path) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
// Format: "pid (comm) state ppid ..."
|
|
||||||
if let Some(rparen) = stat.rfind(')') {
|
if let Some(rparen) = stat.rfind(')') {
|
||||||
let rest = &stat[rparen + 1..];
|
let rest = &stat[rparen + 1..];
|
||||||
let fields: Vec<&str> = rest.split_whitespace().collect();
|
let fields: Vec<&str> = rest.split_whitespace().collect();
|
||||||
@@ -319,7 +405,6 @@ fn read_window_title(c: &X11Conn, xid: u32) -> Option<String> {
|
|||||||
.ok()?;
|
.ok()?;
|
||||||
let s = String::from_utf8_lossy(&reply.value).to_string();
|
let s = String::from_utf8_lossy(&reply.value).to_string();
|
||||||
if s.is_empty() {
|
if s.is_empty() {
|
||||||
// fallback to WM_NAME
|
|
||||||
let r2 = c
|
let r2 = c
|
||||||
.conn
|
.conn
|
||||||
.get_property(false, xid, AtomEnum::WM_NAME, AtomEnum::STRING, 0, 1024)
|
.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)
|
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(())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ export function Canvas({ initialCards }: CanvasProps) {
|
|||||||
return () => unlisten?.();
|
return () => unlisten?.();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Hide embedded apps while the launcher dialog is open so they don't cover it.
|
||||||
|
useEffect(() => {
|
||||||
|
xapp.setAllVisible(!launcherOpen).catch(() => {});
|
||||||
|
}, [launcherOpen]);
|
||||||
|
|
||||||
const onWheel = useCallback((e: React.WheelEvent) => {
|
const onWheel = useCallback((e: React.WheelEvent) => {
|
||||||
if (!e.ctrlKey && !e.metaKey) return;
|
if (!e.ctrlKey && !e.metaKey) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ export const xapp = {
|
|||||||
invoke<void>("app_set_geometry", { xid, x, y, width, height }),
|
invoke<void>("app_set_geometry", { xid, x, y, width, height }),
|
||||||
setVisible: (xid: number, visible: boolean) =>
|
setVisible: (xid: number, visible: boolean) =>
|
||||||
invoke<void>("app_set_visible", { xid, visible }),
|
invoke<void>("app_set_visible", { xid, visible }),
|
||||||
|
setAllVisible: (visible: boolean) =>
|
||||||
|
invoke<void>("app_set_all_visible", { visible }),
|
||||||
close: (xid: number) => invoke<void>("app_close", { xid }),
|
close: (xid: number) => invoke<void>("app_close", { xid }),
|
||||||
onDestroyed: (handler: (xid: number) => void): Promise<UnlistenFn> =>
|
onDestroyed: (handler: (xid: number) => void): Promise<UnlistenFn> =>
|
||||||
listen<number>("app:destroyed", (e) => handler(e.payload)),
|
listen<number>("app:destroyed", (e) => handler(e.payload)),
|
||||||
|
|||||||
Reference in New Issue
Block a user