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:
parent
0ad52a4ce7
commit
9313f249af
3 changed files with 327 additions and 6 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue