Studio: persist Tauri window size and maximized state across launches (#5799)
* Studio: persist Tauri window size and maximized state across launches * Studio: keep window state under the app home dir, not ~/.config * Undo an unnecessary change * Address Gemini's feedback * Revert to tauri-plugin-window-state implementation * fix(Studio): restore saved window size before default layout * Fix cross-platform window-state restore * fix(Studio): avoid clobbering saved window size --------- Co-authored-by: Wasim Yousef Said <wasimysdev@gmail.com> Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com>
This commit is contained in:
parent
7381958225
commit
85692f1c1c
7 changed files with 88 additions and 21 deletions
10
studio/frontend/package-lock.json
generated
10
studio/frontend/package-lock.json
generated
|
|
@ -37,6 +37,7 @@
|
|||
"@tauri-apps/plugin-opener": "^2.5.3",
|
||||
"@tauri-apps/plugin-process": "^2.3.1",
|
||||
"@tauri-apps/plugin-updater": "^2.10.1",
|
||||
"@tauri-apps/plugin-window-state": "^2.4.1",
|
||||
"@toolwind/corner-shape": "^0.0.8-3",
|
||||
"@xyflow/react": "^12.10.0",
|
||||
"assistant-stream": "0.3.12",
|
||||
|
|
@ -6239,6 +6240,15 @@
|
|||
"@tauri-apps/api": "^2.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-window-state": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-window-state/-/plugin-window-state-2.4.1.tgz",
|
||||
"integrity": "sha512-OuvdrzyY8Q5Dbzpj+GcrnV1iCeoZbcFdzMjanZMMcAEUNy/6PH5pxZPXpaZLOR7whlzXiuzx0L9EKZbH7zpdRw==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@toolwind/corner-shape": {
|
||||
"version": "0.0.8-3",
|
||||
"resolved": "https://registry.npmjs.org/@toolwind/corner-shape/-/corner-shape-0.0.8-3.tgz",
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
"@tauri-apps/plugin-opener": "^2.5.3",
|
||||
"@tauri-apps/plugin-process": "^2.3.1",
|
||||
"@tauri-apps/plugin-updater": "^2.10.1",
|
||||
"@tauri-apps/plugin-window-state": "^2.4.1",
|
||||
"@toolwind/corner-shape": "^0.0.8-3",
|
||||
"@xyflow/react": "^12.10.0",
|
||||
"assistant-stream": "0.3.12",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ interface AppProviderProps {
|
|||
type TauriWindowMode = "setup" | "app";
|
||||
type WindowLayoutGuard = () => boolean;
|
||||
|
||||
const MIN_WINDOW_WIDTH = 900;
|
||||
const MIN_WINDOW_HEIGHT = 600;
|
||||
|
||||
async function showSetupWindow(isCurrent: WindowLayoutGuard): Promise<void> {
|
||||
const { getCurrentWindow } = await import("@tauri-apps/api/window");
|
||||
if (!isCurrent()) return;
|
||||
|
|
@ -39,35 +42,54 @@ async function showSetupWindow(isCurrent: WindowLayoutGuard): Promise<void> {
|
|||
|
||||
async function applyAppWindowLayout(isCurrent: WindowLayoutGuard): Promise<void> {
|
||||
const { getCurrentWindow, currentMonitor, LogicalSize } = await import("@tauri-apps/api/window");
|
||||
const { invoke } = await import("@tauri-apps/api/core");
|
||||
const { restoreStateCurrent, StateFlags } = await import("@tauri-apps/plugin-window-state");
|
||||
if (!isCurrent()) return;
|
||||
|
||||
const win = getCurrentWindow();
|
||||
const monitor = await currentMonitor();
|
||||
// Decide first-launch vs restore from the on-disk state file BEFORE touching the
|
||||
// window. Probing the window itself after restoreStateCurrent is unreliable:
|
||||
// on GTK, set_size against a hidden window is deferred until show(), so
|
||||
// innerSize() reads a stale value and any baseline fallback would overwrite the
|
||||
// queued restore. On macOS the same probe works, hence the inconsistency
|
||||
// between previous iterations of this code.
|
||||
const hasSavedState = await invoke<boolean>("has_saved_window_state");
|
||||
if (!isCurrent()) return;
|
||||
|
||||
let finalW = 900;
|
||||
let finalH = 600;
|
||||
|
||||
if (monitor) {
|
||||
const scale = monitor.scaleFactor;
|
||||
const screenW = monitor.size.width / scale;
|
||||
const screenH = monitor.size.height / scale;
|
||||
|
||||
finalW = Math.max(900, Math.round(screenW * 0.75));
|
||||
const targetH = Math.max(600, Math.round(finalW / 1.618));
|
||||
finalH = Math.min(targetH, Math.round(screenH * 0.85));
|
||||
}
|
||||
|
||||
if (!isCurrent()) return;
|
||||
await win.setSize(new LogicalSize(finalW, finalH));
|
||||
if (!isCurrent()) return;
|
||||
await win.setSizeConstraints({ minWidth: 900, minHeight: 600 });
|
||||
if (!isCurrent()) return;
|
||||
await win.setResizable(true);
|
||||
if (!isCurrent()) return;
|
||||
await win.center();
|
||||
|
||||
if (hasSavedState) {
|
||||
// Subsequent launch: the plugin handles size, position, and maximized,
|
||||
// with built-in off-screen protection (monitor-intersection check) for
|
||||
// positions saved on a now-disconnected display.
|
||||
await restoreStateCurrent(
|
||||
StateFlags.SIZE | StateFlags.POSITION | StateFlags.MAXIMIZED,
|
||||
);
|
||||
} else {
|
||||
// First launch: fit to the current monitor and center.
|
||||
const monitor = await currentMonitor();
|
||||
if (!isCurrent()) return;
|
||||
let finalW = MIN_WINDOW_WIDTH;
|
||||
let finalH = MIN_WINDOW_HEIGHT;
|
||||
if (monitor) {
|
||||
const scale = monitor.scaleFactor;
|
||||
const screenW = monitor.size.width / scale;
|
||||
const screenH = monitor.size.height / scale;
|
||||
finalW = Math.max(MIN_WINDOW_WIDTH, Math.round(screenW * 0.75));
|
||||
const targetH = Math.max(MIN_WINDOW_HEIGHT, Math.round(finalW / 1.618));
|
||||
finalH = Math.min(targetH, Math.round(screenH * 0.85));
|
||||
}
|
||||
await win.setSize(new LogicalSize(finalW, finalH));
|
||||
if (!isCurrent()) return;
|
||||
await win.center();
|
||||
}
|
||||
if (!isCurrent()) return;
|
||||
await win.show();
|
||||
if (!isCurrent()) return;
|
||||
// Apply constraints after restore/show. Setting constraints before plugin restore
|
||||
// can emit a Resized event and overwrite the plugin's cached saved size.
|
||||
await win.setSizeConstraints({ minWidth: MIN_WINDOW_WIDTH, minHeight: MIN_WINDOW_HEIGHT });
|
||||
}
|
||||
|
||||
async function showWindowFallback(): Promise<void> {
|
||||
|
|
|
|||
16
studio/src-tauri/Cargo.lock
generated
16
studio/src-tauri/Cargo.lock
generated
|
|
@ -4751,6 +4751,21 @@ dependencies = [
|
|||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-window-state"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.10.1"
|
||||
|
|
@ -5395,6 +5410,7 @@ dependencies = [
|
|||
"tauri-plugin-process",
|
||||
"tauri-plugin-single-instance",
|
||||
"tauri-plugin-updater",
|
||||
"tauri-plugin-window-state",
|
||||
"tokio",
|
||||
"windows 0.62.2",
|
||||
"windows-sys 0.59.0",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ tauri-plugin-clipboard-manager = "2"
|
|||
tauri-plugin-dialog = "2"
|
||||
rand = "0.10.0"
|
||||
tauri-plugin-notification = "2.3.3"
|
||||
tauri-plugin-window-state = "2"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
"allow": [{ "url": "https://*" }, { "url": "http://*" }, { "url": "mailto:*" }]
|
||||
},
|
||||
"updater:default",
|
||||
"clipboard-manager:allow-write-text"
|
||||
"clipboard-manager:allow-write-text",
|
||||
"window-state:default"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,15 @@ use std::fs;
|
|||
use tauri::menu::{MenuBuilder, MenuItemBuilder};
|
||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
||||
use tauri::{Emitter, Manager};
|
||||
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
|
||||
|
||||
#[tauri::command]
|
||||
fn has_saved_window_state(app: tauri::AppHandle) -> bool {
|
||||
let Ok(dir) = app.path().app_config_dir() else {
|
||||
return false;
|
||||
};
|
||||
dir.join(app.filename()).is_file()
|
||||
}
|
||||
|
||||
fn setup_logging() {
|
||||
let mut loggers: Vec<Box<dyn SharedLogger>> = vec![];
|
||||
|
|
@ -173,6 +182,12 @@ fn main() {
|
|||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(
|
||||
tauri_plugin_window_state::Builder::new()
|
||||
.with_state_flags(StateFlags::SIZE | StateFlags::POSITION | StateFlags::MAXIMIZED)
|
||||
.skip_initial_state("main")
|
||||
.build(),
|
||||
)
|
||||
.manage(diagnostics::new_diagnostics_state())
|
||||
.manage(install::new_install_state())
|
||||
.manage(native_intents::new_native_intake_state())
|
||||
|
|
@ -204,6 +219,7 @@ fn main() {
|
|||
native_intents::register_artifact_path,
|
||||
native_intents::reveal_path_token,
|
||||
native_intents::open_path_token,
|
||||
has_saved_window_state,
|
||||
])
|
||||
.setup(|app| {
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue