host: tab placements skip unchanged bounds, one SetBounds per move, and follow window resizes natively

layout.rs (headless, tested): Placements remembers each tab webview's last rectangle and
visibility, so a tabs_sync that changes nothing makes no native call and one that moves one
pane touches one webview; Frame/follow_rect estimate where panes go when the window size
changes (outer margins fixed, proportional in between, shared borders stay shared).

host.rs: place_tab/forget_tab use it; place_child is a single set_bounds (one WebView2
SetBounds instead of a move then a resize, each a page re-layout); follow_window_resizes
re-places visible tabs from the window's own Resized events on the main thread, before the
shell lays out again, and keeps WindowMetrics current so a sync needs no blocking round
trips for the window size; layout_window says when a layout was made for a stale size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
CRTkafa 2026-09-29 03:23:44 +07:00
parent 0ad52a4ce7
commit 9313f249af
3 changed files with 327 additions and 6 deletions

View file

@ -264,11 +264,13 @@ mod helper_close {
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);
// One native move+resize: separate set_position/set_size calls were two WebView2 `SetBounds`
// (the page re-laid out at the old size, then at the new one) and two main-thread messages.
#[cfg(not(target_os = "linux"))]
{
let _ = webview.set_position(tauri::LogicalPosition::new(x, y));
let _ = webview.set_size(tauri::LogicalSize::new(width, height));
}
let _ = webview.set_bounds(tauri::Rect {
position: tauri::LogicalPosition::new(x, y).into(),
size: tauri::LogicalSize::new(width, height).into(),
});
}
/// Linux: the shell webview becomes the base layer of a GtkOverlay filling the window, which
@ -281,8 +283,6 @@ pub fn attach_shell<R: Runtime>(shell: &Webview<R>) {
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).
/// Puts the shell webview above every tab webview (`on_top`), so shell menus, dialogs and drag
/// zones draw over the panes while the panes stay on screen (the shell page must then be
/// transparent where the panes are), or back below them. Returns whether this platform can.
@ -364,6 +364,8 @@ mod macos {
}
}
/// 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())
@ -526,6 +528,11 @@ pub fn set_tab_visible<R: Runtime>(webview: &Webview<R>, visible: bool, parked:
capture_still(webview);
}
let _ = if visible { webview.show() } else { webview.hide() };
set_memory_target(webview, visible, parked);
}
fn set_memory_target<R: Runtime>(webview: &Webview<R>, visible: bool, parked: bool) {
let _ = (webview, visible, parked);
#[cfg(windows)]
{
use std::collections::HashMap;
@ -559,3 +566,113 @@ pub fn set_tab_visible<R: Runtime>(webview: &Webview<R>, visible: bool, parked:
});
}
}
fn placements() -> std::sync::MutexGuard<'static, crate::layout::Placements> {
use std::sync::{Mutex, OnceLock};
static PLACEMENTS: OnceLock<Mutex<crate::layout::Placements>> = OnceLock::new();
PLACEMENTS.get_or_init(Default::default).lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
/// Places a tab webview and shows or hides it ([`place_child`], [`set_tab_visible`]), skipping the
/// native calls for whatever did not change since its last placement: a sync that moves one pane
/// touches one webview, and one that changes nothing costs nothing.
pub fn place_tab<R: Runtime>(webview: &Webview<R>, rect: crate::layout::Rect, visible: bool, parked: bool) {
let changed = placements().place(webview.label(), rect, visible);
if changed.bounds {
let (x, y, width, height) = rect;
place_child(webview, x, y, width, height);
}
if changed.visible {
set_tab_visible(webview, visible, parked);
} else {
set_memory_target(webview, visible, parked);
}
}
/// A tab webview was closed: a new one with the same label is placed from scratch.
pub fn forget_tab(label: &str) {
placements().forget(label);
}
/// The window size (logical px) and state a tab layout is clamped against.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WindowMetrics {
pub width: f64,
pub height: f64,
pub maximized: bool,
pub frameless: bool,
}
fn metrics_cache() -> std::sync::MutexGuard<'static, Option<WindowMetrics>> {
use std::sync::{Mutex, OnceLock};
static METRICS: OnceLock<Mutex<Option<WindowMetrics>>> = OnceLock::new();
METRICS.get_or_init(Default::default).lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn read_metrics<R: Runtime>(window: &tauri::Window<R>) -> tauri::Result<WindowMetrics> {
let size = window.inner_size()?.to_logical::<f64>(window.scale_factor()?);
Ok(WindowMetrics {
width: size.width,
height: size.height,
maximized: window.is_maximized().unwrap_or(false),
frameless: !window.is_decorated().unwrap_or(true),
})
}
/// The window's metrics, kept current by [`follow_window_resizes`]: a tab sync (off the main
/// thread) then needs no blocking round trip to the event loop for them.
pub fn window_metrics<R: Runtime>(window: &tauri::Window<R>) -> tauri::Result<WindowMetrics> {
if let Some(metrics) = *metrics_cache() {
return Ok(metrics);
}
let metrics = read_metrics(window)?;
*metrics_cache() = Some(metrics);
Ok(metrics)
}
/// The shell laid its panes out for a window of `viewport` (its own inner size); returns the
/// window size to lay them out in now and whether the layout is stale (the window was resized
/// since). Stale layouts are re-based with [`crate::layout::follow_rect`] by the caller.
pub fn layout_window(metrics: &WindowMetrics, viewport: Option<(f64, f64)>) -> Option<(f64, f64)> {
let (width, height) = viewport.filter(|(w, h)| w.is_finite() && h.is_finite() && *w > 1.0 && *h > 1.0)?;
let stale = (width - metrics.width).abs() > 1.5 || (height - metrics.height).abs() > 1.5;
stale.then_some((width, height))
}
/// Records the window size the tab placements now correspond to.
pub fn layout_placed(metrics: &WindowMetrics) {
placements().set_window((metrics.width, metrics.height));
}
/// Keeps tab webviews glued to the window while it is resized, maximized or snapped: on every
/// resize (main thread, in the same event the runtime resizes the shell webview in) the visible
/// tabs are re-placed from the last layout ([`crate::layout::Placements::follow`]), with no JS
/// round trip. The shell's exact layout follows a frame or two later and replaces the estimate.
/// Call once from `setup`, after the window is built.
pub fn follow_window_resizes<R: Runtime>(window: &tauri::Window<R>) {
if let Ok(metrics) = read_metrics(window) {
*metrics_cache() = Some(metrics);
}
let target = window.clone();
window.on_window_event(move |event| {
if !matches!(event, tauri::WindowEvent::Resized(_) | tauri::WindowEvent::ScaleFactorChanged { .. }) {
return;
}
let Ok(metrics) = read_metrics(&target) else { return };
*metrics_cache() = Some(metrics);
// Minimized: 0x0; nothing to follow, and the next restore re-bases from the last layout.
if metrics.width < 2.0 || metrics.height < 2.0 {
return;
}
let moved = placements().follow((metrics.width, metrics.height));
for (label, (x, y, width, height)) in moved {
if let Some(webview) = target.app_handle().get_webview(&label) {
// Never over the titlebar or the resize border, like every shell layout.
let clamped = policy::clamp_tab_rect(x, y, width, height, metrics.width, metrics.height, metrics.maximized, metrics.frameless);
if let Ok(Some((x, y, width, height))) = clamped {
place_child(&webview, x, y, width, height);
}
}
}
});
}