diff --git a/desktop/tauri/src-tauri/src/main.rs b/desktop/tauri/src-tauri/src/main.rs index 0773b130..69a14ee8 100644 --- a/desktop/tauri/src-tauri/src/main.rs +++ b/desktop/tauri/src-tauri/src/main.rs @@ -24,14 +24,22 @@ use log::{debug, error, info}; use portmaster::PortmasterExt; use tauri_plugin_log::RotationStrategy; use traymenu::setup_tray_menu; -use window::{close_splash_window, create_main_window, hide_splash_window}; -use tauri_plugin_window_state::StateFlags; +use window::{close_splash_window, create_main_window, hide_splash_window, create_debounced_window_state_saver}; +use tauri_plugin_window_state::{AppHandleExt, StateFlags}; #[macro_use] extern crate lazy_static; const FALLBACK_TO_OLD_UI_EXIT_CODE: i32 = 77; +// Window state configuration: save all properties except visibility to avoid +// interference with the "--background" command line argument +const WINDOW_STATE_FLAGS_TO_SAVE: StateFlags = StateFlags::from_bits_truncate( + StateFlags::all().bits() & !StateFlags::VISIBLE.bits() +); +// Default timeout for debounced window state saving +const WINDOW_STATE_SAVE_TIMEOUT: Duration = Duration::from_secs(3); + #[derive(Clone, serde::Serialize)] struct Payload { args: Vec, @@ -177,7 +185,7 @@ fn main() { // Initialize save windows state plugin. .plugin(tauri_plugin_window_state::Builder::default() // Don't save visibility state, so it will not interfere with "--background" command line argument - .with_state_flags(StateFlags::all() & !StateFlags::VISIBLE) + .with_state_flags(WINDOW_STATE_FLAGS_TO_SAVE) // Don't save splash window state .with_denylist(&["splash",]) .build()) @@ -228,6 +236,15 @@ fn main() { // register the custom handler app.portmaster().register_handler(handler); + // Setup window state saving on move/resize events with debouncing + let save_trigger = create_debounced_window_state_saver(app, + WINDOW_STATE_FLAGS_TO_SAVE, + WINDOW_STATE_SAVE_TIMEOUT); + let tx_move = save_trigger.clone(); + let tx_resize = save_trigger; + app.listen_any("tauri://move", move |_event| { let _ = tx_move.try_send(()); }); + app.listen_any("tauri://resize", move |_event| { let _ = tx_resize.try_send(()); }); + Ok(()) }) .any_thread() @@ -255,6 +272,10 @@ fn main() { label ); + // Manually save the window state on close attempt. + // This ensures the state is saved since we prevent the close event. + let _ = handle.save_window_state(WINDOW_STATE_FLAGS_TO_SAVE); + api.prevent_close(); if let Some(window) = handle.get_webview_window(label.as_str()) { let result = window.emit("exit-requested", ""); diff --git a/desktop/tauri/src-tauri/src/window.rs b/desktop/tauri/src-tauri/src/window.rs index fc56d66f..c789a97d 100644 --- a/desktop/tauri/src-tauri/src/window.rs +++ b/desktop/tauri/src-tauri/src/window.rs @@ -4,6 +4,7 @@ use tauri::{ WebviewWindow, WebviewWindowBuilder, }; use std::sync::{atomic::{AtomicBool, Ordering}}; +use tauri_plugin_window_state::{AppHandleExt, StateFlags}; use crate::{portmaster::PortmasterExt, traymenu}; @@ -262,3 +263,52 @@ pub fn may_navigate_to_ui(win: &mut WebviewWindow, force: bool) { ); } } + +/// Creates a debounced window state saver that waits for a quiet period before saving. +/// +/// Returns a sender that can be used to trigger save events. Multiple rapid events +/// will be debounced - only saving after the specified timeout with no new events. +/// +/// # Example +/// ```rust +/// let save_trigger = create_debounced_window_state_saver(app, state_flags, Duration::from_secs(5)); +/// let _ = save_trigger.try_send(()); // Trigger a save (will be debounced) +/// ``` +pub fn create_debounced_window_state_saver( + app: &tauri::App, + state_flags: StateFlags, + debounce_timeout: std::time::Duration, +) -> tokio::sync::mpsc::Sender<()> { + + let app_handle = app.handle().clone(); + let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(10); + + // Spawn debouncer task - saves state after the specified timeout following the last event + tauri::async_runtime::spawn(async move { + loop { + if rx.recv().await.is_none() { + break; // Channel closed + } + + loop { + match tokio::time::timeout(debounce_timeout, rx.recv()).await { + Ok(Some(_)) => { + continue; // Received another event within timeout period, restart the timer + } + Ok(None) => { + return; // Channel closed + } + Err(_) => { + // Timeout: specified duration passed without new events, save state + if let Err(e) = app_handle.save_window_state(state_flags) { + debug!("Failed to save window state: {}", e); + } + break; // Exit inner loop and wait for next event + } + } + } + } + }); + + tx +}