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 <noreply@anthropic.com>
This commit is contained in:
CRTkafa 2026-09-28 21:40:51 +07:00
parent edf5171435
commit 1486f31a66
3 changed files with 103 additions and 1 deletions

View file

@ -3807,6 +3807,7 @@ version = "0.1.0"
dependencies = [
"base64 0.22.1",
"chrono",
"gtk",
"minisign-verify",
"serde",
"serde_json",

View file

@ -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"

View file

@ -185,6 +185,103 @@ pub fn system_menu<R: Runtime>(webview: &Webview<R>, 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<R: Runtime>(webview: &Webview<R>, 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<R: Runtime>(shell: &Webview<R>) {
#[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<R: Runtime>(webview: &Webview<R>) -> 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<String, Rect>> {
static RECTS: OnceLock<Mutex<HashMap<String, Rect>>> = OnceLock::new();
RECTS.get_or_init(Default::default).lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub fn placed(label: &str) -> Option<Rect> {
rects().get(label).copied()
}
pub fn attach_shell<R: Runtime>(shell: &Webview<R>) {
let _ = shell.with_webview(|platform| {
let widget = platform.inner().upcast::<gtk::Widget>();
let Some(vbox) = widget.parent().and_then(|parent| parent.downcast::<gtk::Box>().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::<GtkRect>(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<R: Runtime>(webview: &Webview<R>, 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::<gtk::Widget>();
// SAFETY: read back only as `GtkRect` (the overlay's get-child-position handler).
unsafe { widget.set_data::<GtkRect>(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::<gtk::Overlay>().is_none() {
// First placement: out of the window box (where tauri packed it), into the overlay.
let Ok(vbox) = parent.downcast::<gtk::Box>() else { return };
let Some(overlay) = vbox.children().into_iter().find_map(|child| child.downcast::<gtk::Overlay>().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