feat: implement debounced window state saving for shutdown resilience
- Add create_debounced_window_state_saver() function in window.rs - Extract WINDOW_STATE_FLAGS_TO_SAVE and WINDOW_STATE_SAVE_TIMEOUT constants - Listen to tauri://move and tauri://resize events with 5-second debouncing - Automatically save window state after positioning changes settle - Prevent window position loss during system shutdowns/restarts The debouncing mechanism avoids excessive disk I/O during active window manipulation while ensuring recent position changes are preserved even when the application doesn't close normally. Fixes window position being lost when system restarts before user manually closes the application. https://github.com/safing/portmaster/issues/2011
This commit is contained in:
@@ -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<String>,
|
||||
@@ -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", "");
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user