From 1486f31a660085a65cc71586c4321c99197af362 Mon Sep 17 00:00:00 2001 From: CRT Date: Mon, 28 Sep 2026 21:40:51 +0700 Subject: [PATCH] host: place child webviews on Linux in a GtkOverlay over the shell tauri 2 (runtime-wry build_gtk with the window's GtkBox) packs every child webview into the window box, stacked and with set_position/set_size ignored, so tab webviews landed under the shell instead of over their panes. place_child/attach_shell move them into a GtkOverlay (shell as base layer) and allocate each at its exact rect via get-child-position; placed_rect reports the placement (GTK children have no position). Windows/macOS keep set_position/set_size. Co-Authored-By: Claude Opus 5.5 --- crates/waybetter-host/Cargo.lock | 1 + crates/waybetter-host/Cargo.toml | 6 +- crates/waybetter-host/src/host.rs | 97 +++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/crates/waybetter-host/Cargo.lock b/crates/waybetter-host/Cargo.lock index 0e05bdd..7803c2c 100644 --- a/crates/waybetter-host/Cargo.lock +++ b/crates/waybetter-host/Cargo.lock @@ -3807,6 +3807,7 @@ version = "0.1.0" dependencies = [ "base64 0.22.1", "chrono", + "gtk", "minisign-verify", "serde", "serde_json", diff --git a/crates/waybetter-host/Cargo.toml b/crates/waybetter-host/Cargo.toml index 365c53b..339ba98 100644 --- a/crates/waybetter-host/Cargo.toml +++ b/crates/waybetter-host/Cargo.toml @@ -14,7 +14,7 @@ path = "src/lib.rs" [features] default = [] # Tauri glue (webview helpers, window system menu). Off for headless tests. -tauri = ["dep:tauri", "dep:windows", "dep:webview2-com"] +tauri = ["dep:tauri", "dep:windows", "dep:webview2-com", "dep:gtk"] [dependencies] serde = { version = "1", features = ["derive"] } @@ -31,5 +31,9 @@ windows = { version = "0.62", features = ["Win32_Foundation", "Win32_UI_WindowsA # The WebView2 COM interfaces wry already links (hidden tabs: low memory target). webview2-com = { version = "0.39", optional = true } +[target.'cfg(target_os = "linux")'.dependencies] +# The GTK version tauri/wry link: child webviews are laid out in a GtkOverlay (host::place_child). +gtk = { version = "0.18", optional = true } + [dev-dependencies] tempfile = "3" diff --git a/crates/waybetter-host/src/host.rs b/crates/waybetter-host/src/host.rs index 1127b03..b61d15a 100644 --- a/crates/waybetter-host/src/host.rs +++ b/crates/waybetter-host/src/host.rs @@ -185,6 +185,103 @@ pub fn system_menu(webview: &Webview, x: f64, y: f64) -> Result<( Ok(()) } +/// Places a child webview (tab, popover) at a rectangle of its window, in logical px. +/// +/// Windows/macOS: the native child bounds. Linux: tauri 2 packs every child webview into the +/// window's GtkBox (`build_gtk` with a box: stacked one under another, bounds ignored), so the host +/// moves each one into the GtkOverlay made by [`attach_shell`] and places it there. +pub fn place_child(webview: &Webview, x: f64, y: f64, width: f64, height: f64) { + #[cfg(target_os = "linux")] + linux::place(webview, x, y, width, height); + #[cfg(not(target_os = "linux"))] + { + let _ = webview.set_position(tauri::LogicalPosition::new(x, y)); + let _ = webview.set_size(tauri::LogicalSize::new(width, height)); + } +} + +/// Linux: the shell webview becomes the base layer of a GtkOverlay filling the window, which +/// [`place_child`] layers tab webviews over. Call once, right after the shell is added. No-op +/// elsewhere. +pub fn attach_shell(shell: &Webview) { + #[cfg(target_os = "linux")] + linux::attach_shell(shell); + #[cfg(not(target_os = "linux"))] + let _ = shell; +} + +/// Where [`place_child`] last put a child webview, in logical px (x, y, width, height). Linux +/// only: there `Webview::position` is not the placement (always 0,0 outside X11 child windows). +#[cfg(target_os = "linux")] +pub fn placed_rect(webview: &Webview) -> Option<(f64, f64, f64, f64)> { + linux::placed(webview.label()) +} + +#[cfg(target_os = "linux")] +mod linux { + use gtk::prelude::*; + use std::collections::HashMap; + use std::sync::{Mutex, OnceLock}; + use tauri::{Runtime, Webview}; + + type Rect = (f64, f64, f64, f64); + /// A child's placement in whole px (x, y, width, height), kept on the GTK widget. + type GtkRect = (i32, i32, i32, i32); + const RECT_KEY: &str = "waybetter-rect"; + + fn rects() -> std::sync::MutexGuard<'static, HashMap> { + static RECTS: OnceLock>> = OnceLock::new(); + RECTS.get_or_init(Default::default).lock().unwrap_or_else(|poisoned| poisoned.into_inner()) + } + + pub fn placed(label: &str) -> Option { + rects().get(label).copied() + } + + pub fn attach_shell(shell: &Webview) { + let _ = shell.with_webview(|platform| { + let widget = platform.inner().upcast::(); + let Some(vbox) = widget.parent().and_then(|parent| parent.downcast::().ok()) else { return }; + let overlay = gtk::Overlay::new(); + // Exactly the placed rectangle: the default position uses the child's preferred size, + // which WebKit (not the placement) decides. + overlay.connect_get_child_position(|_, child| { + // SAFETY: "waybetter-rect" is only ever set by `place`, always as `GtkRect`. + let (x, y, w, h) = unsafe { *child.data::(RECT_KEY)?.as_ptr() }; + Some(gtk::gdk::Rectangle::new(x, y, w, h)) + }); + // `widget` holds a reference, so removing it from the box does not destroy it. + vbox.remove(&widget); + overlay.add(&widget); + vbox.pack_start(&overlay, true, true, 0); + overlay.show(); + }); + } + + pub fn place(webview: &Webview, x: f64, y: f64, width: f64, height: f64) { + rects().insert(webview.label().to_owned(), (x, y, width, height)); + // Whole px, with the right/bottom edges rounded the same way as the left/top ones. + let (left, top) = (x.round(), y.round()); + let (w, h) = (((x + width).round() - left).max(1.0), ((y + height).round() - top).max(1.0)); + let _ = webview.with_webview(move |platform| { + let widget = platform.inner().upcast::(); + // SAFETY: read back only as `GtkRect` (the overlay's get-child-position handler). + unsafe { widget.set_data::(RECT_KEY, (left as i32, top as i32, w as i32, h as i32)) }; + let Some(parent) = widget.parent() else { return }; + if parent.downcast_ref::().is_none() { + // First placement: out of the window box (where tauri packed it), into the overlay. + let Ok(vbox) = parent.downcast::() else { return }; + let Some(overlay) = vbox.children().into_iter().find_map(|child| child.downcast::().ok()) else { + return; + }; + vbox.remove(&widget); + overlay.add_overlay(&widget); + } + widget.queue_resize(); + }); + } +} + /// Shows or hides a tab webview and sets its WebView2 memory target to match /// ([`tabs::memory_target`]): hidden tabs run at `COREWEBVIEW2_MEMORY_USAGE_TARGET_LEVEL_LOW` /// (ICoreWebView2_19, WebView2 Runtime 114+; older runtimes just skip it). Other platforms only