Skip to content

Commit 7c566e3

Browse files
committed
feat(window):save window state before handling close requests
1 parent 1ac344d commit 7c566e3

6 files changed

Lines changed: 41 additions & 201 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ tauri-plugin-notification = "2.3"
178178
tauri-plugin-updater = "2.10"
179179
tauri-plugin-global-shortcut = "2.3"
180180
tauri-plugin-single-instance = "2.4"
181+
tauri-plugin-window-state = "2.4"
181182
tauri-build = { version = "2.6", features = [] }
182183

183184
# Desktop support

src/apps/desktop/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ tauri-plugin-notification = { workspace = true }
4242
tauri-plugin-updater = { workspace = true }
4343
tauri-plugin-global-shortcut = { workspace = true }
4444
tauri-plugin-single-instance = { workspace = true }
45+
tauri-plugin-window-state = { workspace = true }
4546
# Keep Tauri's transitive time resolution on the known-good release in CI,
4647
# where the root Cargo.lock is intentionally ignored.
4748
time = { workspace = true }

src/apps/desktop/src/api/system_api.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ pub struct RestartAppRequest {}
196196
pub async fn restart_app(app: AppHandle, request: RestartAppRequest) -> Result<(), String> {
197197
let _ = request;
198198
crate::crash_diagnostics::mark_clean_shutdown("restart_app");
199+
crate::save_main_window_state(&app);
199200
crate::perform_process_exit_cleanup();
200201
app.restart();
201202
Ok(())

src/apps/desktop/src/lib.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use std::sync::{
2828
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
2929
use tauri::Emitter;
3030
use tauri::Manager;
31+
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
3132

3233
// Re-export API
3334
pub use api::*;
@@ -250,6 +251,16 @@ fn handle_secondary_launch(app: &tauri::AppHandle) {
250251
}
251252
}
252253

254+
fn main_window_state_flags() -> StateFlags {
255+
StateFlags::SIZE | StateFlags::POSITION | StateFlags::MAXIMIZED | StateFlags::FULLSCREEN
256+
}
257+
258+
pub(crate) fn save_main_window_state(app: &tauri::AppHandle) {
259+
if let Err(error) = app.save_window_state(main_window_state_flags()) {
260+
log::warn!("Failed to save main window state: {}", error);
261+
}
262+
}
263+
253264
#[tauri::command]
254265
async fn webdriver_bridge_result(request: WebdriverBridgeResultRequest) -> Result<(), String> {
255266
log::debug!("webdriver_bridge_result command invoked");
@@ -446,6 +457,12 @@ pub async fn run() {
446457
)
447458
.plugin(tauri_plugin_notification::init())
448459
.plugin(tauri_plugin_updater::Builder::new().build())
460+
.plugin(
461+
tauri_plugin_window_state::Builder::default()
462+
.with_state_flags(main_window_state_flags())
463+
.with_filter(|label| label == "main")
464+
.build(),
465+
)
449466
.manage(app_state)
450467
.manage(desktop_runtime)
451468
.manage(coordinator_state)
@@ -836,6 +853,12 @@ pub async fn run() {
836853
})
837854
.on_window_event({
838855
move |window, event| {
856+
if window.label() == "main"
857+
&& matches!(event, tauri::WindowEvent::CloseRequested { .. })
858+
{
859+
save_main_window_state(window.app_handle());
860+
}
861+
839862
if let tauri::WindowEvent::CloseRequested { api: _api, .. } = event {
840863
if window.label() == "main" {
841864
#[cfg(target_os = "macos")]

src/apps/desktop/src/theme.rs

Lines changed: 1 addition & 190 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
//! Theme System
22
33
use std::sync::{OnceLock, RwLock};
4-
#[cfg(target_os = "windows")]
5-
use std::time::Duration;
64
use std::time::Instant;
75

86
use bitfun_core::infrastructure::try_get_path_manager_arc;
@@ -20,109 +18,13 @@ const AGENT_COMPANION_WINDOW_MAX_WIDTH: f64 = 360.0;
2018
const AGENT_COMPANION_WINDOW_MAX_HEIGHT: f64 = 240.0;
2119
const AGENT_COMPANION_WINDOW_MARGIN: i32 = 64;
2220
const AGENT_COMPANION_WINDOW_EDGE_MARGIN: f64 = 8.0;
23-
#[cfg(target_os = "windows")]
24-
const WINDOWS_STARTUP_MAXIMIZE_SHOW_WAIT_MAX: Duration = Duration::from_millis(150);
25-
#[cfg(target_os = "windows")]
26-
const WINDOWS_STARTUP_MAXIMIZE_SHOW_WAIT_MIN: Duration = Duration::from_millis(16);
27-
#[cfg(target_os = "windows")]
28-
const WINDOWS_STARTUP_MAXIMIZE_SHOW_WAIT_POLL: Duration = Duration::from_millis(8);
29-
3021
static AGENT_COMPANION_WINDOW_OPS: OnceLock<tokio::sync::Mutex<()>> = OnceLock::new();
3122
static AGENT_COMPANION_WINDOW_LAST_POSITION: OnceLock<RwLock<Option<tauri::LogicalPosition<f64>>>> =
3223
OnceLock::new();
3324
static STARTUP_THEME_BOOTSTRAP_MANIFEST: OnceLock<StartupThemeBootstrapManifest> = OnceLock::new();
3425

3526
const STARTUP_THEME_BOOTSTRAP_JSON: &str = include_str!("generated/startup_theme_bootstrap.json");
3627

37-
#[cfg(target_os = "windows")]
38-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39-
enum WindowsMaximizeShowWaitAction {
40-
Ready,
41-
Sleep(Duration),
42-
TimedOut,
43-
}
44-
45-
#[cfg(target_os = "windows")]
46-
fn windows_maximize_show_wait_action(
47-
is_maximized: Option<bool>,
48-
elapsed: Duration,
49-
) -> WindowsMaximizeShowWaitAction {
50-
if is_maximized == Some(true) && elapsed >= WINDOWS_STARTUP_MAXIMIZE_SHOW_WAIT_MIN {
51-
return WindowsMaximizeShowWaitAction::Ready;
52-
}
53-
54-
if elapsed >= WINDOWS_STARTUP_MAXIMIZE_SHOW_WAIT_MAX {
55-
return WindowsMaximizeShowWaitAction::TimedOut;
56-
}
57-
58-
let target_wait = if is_maximized == Some(true) {
59-
WINDOWS_STARTUP_MAXIMIZE_SHOW_WAIT_MIN
60-
} else {
61-
WINDOWS_STARTUP_MAXIMIZE_SHOW_WAIT_MAX
62-
};
63-
WindowsMaximizeShowWaitAction::Sleep(
64-
target_wait
65-
.saturating_sub(elapsed)
66-
.min(WINDOWS_STARTUP_MAXIMIZE_SHOW_WAIT_POLL),
67-
)
68-
}
69-
70-
#[cfg(target_os = "windows")]
71-
fn read_windows_maximize_state(
72-
window: &tauri::WebviewWindow,
73-
logged_error: &mut bool,
74-
) -> Option<bool> {
75-
match window.is_maximized() {
76-
Ok(is_maximized) => Some(is_maximized),
77-
Err(error) => {
78-
if !*logged_error {
79-
warn!(
80-
"Failed to read main window maximize state during startup show wait: {}",
81-
error
82-
);
83-
*logged_error = true;
84-
}
85-
None
86-
}
87-
}
88-
}
89-
90-
#[cfg(target_os = "windows")]
91-
fn wait_for_windows_maximize_before_show(window: &tauri::WebviewWindow) -> &'static str {
92-
let started_at = Instant::now();
93-
let mut logged_error = false;
94-
95-
loop {
96-
match windows_maximize_show_wait_action(
97-
read_windows_maximize_state(window, &mut logged_error),
98-
started_at.elapsed(),
99-
) {
100-
WindowsMaximizeShowWaitAction::Ready => return "ready",
101-
WindowsMaximizeShowWaitAction::TimedOut => return "timeout",
102-
WindowsMaximizeShowWaitAction::Sleep(duration) => std::thread::sleep(duration),
103-
}
104-
}
105-
}
106-
107-
#[cfg(target_os = "windows")]
108-
async fn wait_for_windows_maximize_before_show_async(
109-
window: &tauri::WebviewWindow,
110-
) -> &'static str {
111-
let started_at = Instant::now();
112-
let mut logged_error = false;
113-
114-
loop {
115-
match windows_maximize_show_wait_action(
116-
read_windows_maximize_state(window, &mut logged_error),
117-
started_at.elapsed(),
118-
) {
119-
WindowsMaximizeShowWaitAction::Ready => return "ready",
120-
WindowsMaximizeShowWaitAction::TimedOut => return "timeout",
121-
WindowsMaximizeShowWaitAction::Sleep(duration) => tokio::time::sleep(duration).await,
122-
}
123-
}
124-
}
125-
12628
fn agent_companion_window_ops() -> &'static tokio::sync::Mutex<()> {
12729
AGENT_COMPANION_WINDOW_OPS.get_or_init(|| tokio::sync::Mutex::new(()))
12830
}
@@ -575,6 +477,7 @@ pub fn create_main_window(
575477
let mut builder = tauri::WebviewWindowBuilder::new(app_handle, "main", main_url)
576478
.title("BitFun")
577479
.inner_size(1200.0, 800.0)
480+
.center()
578481
.resizable(true)
579482
.fullscreen(false)
580483
.visible(false)
@@ -660,34 +563,6 @@ fn show_main_window_for_startup(
660563
total_started_at: Instant,
661564
startup_trace: &DesktopStartupTrace,
662565
) {
663-
#[cfg(target_os = "windows")]
664-
{
665-
let step_started_at = Instant::now();
666-
if let Err(error) = window.maximize() {
667-
warn!("Failed to maximize main window during startup: {}", error);
668-
} else {
669-
startup_trace.record_elapsed_step("native_window", "windows_maximize", step_started_at);
670-
debug!(
671-
"Main window startup show step completed: step=maximize duration_ms={} since_create_start_ms={}",
672-
step_started_at.elapsed().as_millis(),
673-
total_started_at.elapsed().as_millis()
674-
);
675-
}
676-
let show_delay_started_at = Instant::now();
677-
let show_wait_outcome = wait_for_windows_maximize_before_show(window);
678-
startup_trace.record_elapsed_step(
679-
"native_window",
680-
"windows_show_after_maximize_wait",
681-
show_delay_started_at,
682-
);
683-
debug!(
684-
"Main window startup show step completed: step=wait_for_maximize_state outcome={} duration_ms={} since_create_start_ms={}",
685-
show_wait_outcome,
686-
show_delay_started_at.elapsed().as_millis(),
687-
total_started_at.elapsed().as_millis()
688-
);
689-
}
690-
691566
let show_started_at = Instant::now();
692567
if let Err(error) = window.show() {
693568
warn!("Failed to show main window during startup: {}", error);
@@ -1012,29 +887,6 @@ pub async fn hide_agent_companion_desktop_pet(app: tauri::AppHandle) -> Result<(
1012887
pub async fn show_main_window(app: tauri::AppHandle) -> Result<(), String> {
1013888
let total_started_at = Instant::now();
1014889
if let Some(main_window) = app.get_webview_window("main") {
1015-
#[cfg(target_os = "windows")]
1016-
{
1017-
// Work around Windows startup flicker: avoid creating the native window
1018-
// in maximized mode, and maximize it right before showing instead.
1019-
let step_started_at = Instant::now();
1020-
main_window.maximize().map_err(|e| {
1021-
error!("Failed to maximize main window: {}", e);
1022-
format!("Failed to maximize main window: {}", e)
1023-
})?;
1024-
debug!(
1025-
"Main window show step completed: step=maximize duration_ms={}",
1026-
step_started_at.elapsed().as_millis()
1027-
);
1028-
1029-
let wait_started_at = Instant::now();
1030-
let show_wait_outcome = wait_for_windows_maximize_before_show_async(&main_window).await;
1031-
debug!(
1032-
"Main window show step completed: step=wait_for_maximize_state outcome={} duration_ms={}",
1033-
show_wait_outcome,
1034-
wait_started_at.elapsed().as_millis()
1035-
);
1036-
}
1037-
1038890
let step_started_at = Instant::now();
1039891
main_window.show().map_err(|e| {
1040892
error!("Failed to show main window: {}", e);
@@ -1071,44 +923,3 @@ pub async fn show_main_window(app: tauri::AppHandle) -> Result<(), String> {
1071923
);
1072924
Ok(())
1073925
}
1074-
1075-
#[cfg(test)]
1076-
mod tests {
1077-
#[cfg(target_os = "windows")]
1078-
use super::*;
1079-
1080-
#[cfg(target_os = "windows")]
1081-
#[test]
1082-
fn windows_maximize_show_wait_releases_when_maximized() {
1083-
assert_eq!(
1084-
windows_maximize_show_wait_action(Some(true), Duration::ZERO),
1085-
WindowsMaximizeShowWaitAction::Sleep(WINDOWS_STARTUP_MAXIMIZE_SHOW_WAIT_POLL)
1086-
);
1087-
assert_eq!(
1088-
windows_maximize_show_wait_action(Some(true), WINDOWS_STARTUP_MAXIMIZE_SHOW_WAIT_MIN),
1089-
WindowsMaximizeShowWaitAction::Ready
1090-
);
1091-
}
1092-
1093-
#[cfg(target_os = "windows")]
1094-
#[test]
1095-
fn windows_maximize_show_wait_polls_until_max_wait() {
1096-
assert_eq!(
1097-
windows_maximize_show_wait_action(Some(false), Duration::from_millis(20)),
1098-
WindowsMaximizeShowWaitAction::Sleep(WINDOWS_STARTUP_MAXIMIZE_SHOW_WAIT_POLL)
1099-
);
1100-
assert_eq!(
1101-
windows_maximize_show_wait_action(None, Duration::from_millis(148)),
1102-
WindowsMaximizeShowWaitAction::Sleep(Duration::from_millis(2))
1103-
);
1104-
}
1105-
1106-
#[cfg(target_os = "windows")]
1107-
#[test]
1108-
fn windows_maximize_show_wait_times_out_at_original_bound() {
1109-
assert_eq!(
1110-
windows_maximize_show_wait_action(Some(false), WINDOWS_STARTUP_MAXIMIZE_SHOW_WAIT_MAX),
1111-
WindowsMaximizeShowWaitAction::TimedOut
1112-
);
1113-
}
1114-
}

src/web-ui/src/app/startup/startupPerformanceContract.test.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -168,18 +168,21 @@ describe('startup performance contract', () => {
168168
);
169169
});
170170

171-
it('keeps Windows startup show wait state-driven instead of fixed-delay only', () => {
171+
it('centers the first main window and persists geometry before close handling', () => {
172172
const desktopThemeSource = readSource('../../../../apps/desktop/src/theme.rs');
173-
174-
expect(desktopThemeSource).toContain('WINDOWS_STARTUP_MAXIMIZE_SHOW_WAIT_MAX');
175-
expect(desktopThemeSource).toContain('WINDOWS_STARTUP_MAXIMIZE_SHOW_WAIT_MIN');
176-
expect(desktopThemeSource).toContain('WINDOWS_STARTUP_MAXIMIZE_SHOW_WAIT_POLL');
177-
expect(desktopThemeSource).toContain('windows_maximize_show_wait_action');
178-
expect(desktopThemeSource).toContain('window.is_maximized()');
179-
expect(desktopThemeSource).toContain('windows_show_after_maximize_wait');
180-
expect(desktopThemeSource).not.toContain(
181-
'std::thread::sleep(std::time::Duration::from_millis(150))'
182-
);
173+
const desktopLibSource = readSource('../../../../apps/desktop/src/lib.rs');
174+
const windowEventStart = desktopLibSource.indexOf('.on_window_event({');
175+
const invokeHandlerStart = desktopLibSource.indexOf('.invoke_handler(', windowEventStart);
176+
const windowEventSource = desktopLibSource.slice(windowEventStart, invokeHandlerStart);
177+
178+
expect(desktopThemeSource).toContain('.inner_size(1200.0, 800.0)\n .center()');
179+
expect(desktopThemeSource).not.toContain('windows_maximize_show_wait_action');
180+
expect(desktopLibSource).toContain('tauri_plugin_window_state::Builder::default()');
181+
expect(desktopLibSource).toContain('.with_filter(|label| label == "main")');
182+
expect(windowEventStart).toBeGreaterThan(-1);
183+
expect(invokeHandlerStart).toBeGreaterThan(windowEventStart);
184+
expect(windowEventSource).toContain('matches!(event, tauri::WindowEvent::CloseRequested { .. })');
185+
expect(windowEventSource).toContain('save_main_window_state(window.app_handle())');
183186
});
184187

185188
it('keeps system tray creation out of the synchronous Tauri setup path', () => {

0 commit comments

Comments
 (0)