Skip to content

Commit b5e131d

Browse files
committed
feat(desktop): persist main window size
1 parent 4e01279 commit b5e131d

File tree

3 files changed

+193
-38
lines changed

3 files changed

+193
-38
lines changed

desktop/src-tauri/src/main.rs

Lines changed: 173 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use std::time::{Duration, Instant};
2020
use tauri::image::Image;
2121
use tauri::menu::{Menu, MenuBuilder, MenuItemBuilder};
2222
use tauri::tray::TrayIconBuilder;
23-
use tauri::{AppHandle, Emitter, Manager, Runtime};
23+
use tauri::{AppHandle, Emitter, LogicalSize, Manager, Runtime, Size};
2424
use tauri_plugin_updater::{Update, UpdaterExt};
2525

2626
#[cfg(windows)]
@@ -44,6 +44,9 @@ static UPDATE_MANAGER: Lazy<Mutex<UpdateManagerState>> =
4444
const DEFAULT_PROXY_HOST: &str = "127.0.0.1";
4545
const DEFAULT_PROXY_PORT: u16 = 6789;
4646
const SETTINGS_CACHE_FILE: &str = "desktop-settings.json";
47+
const MAIN_WINDOW_LABEL: &str = "main";
48+
const MAIN_WINDOW_MIN_WIDTH: u32 = 1024;
49+
const MAIN_WINDOW_MIN_HEIGHT: u32 = 700;
4750
const LAUNCH_AGENT_LABEL: &str = "com.aigate.desktop";
4851
const TRAY_ID: &str = "aigate-tray";
4952
const MENU_OPEN_MAIN: &str = "open-main";
@@ -74,6 +77,18 @@ const UPDATER_PUBKEY_BASE64: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1Ymx
7477
#[cfg(windows)]
7578
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
7679

80+
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
81+
struct WindowSizeCache {
82+
width: u32,
83+
height: u32,
84+
}
85+
86+
impl WindowSizeCache {
87+
fn as_tauri_size(self) -> Size {
88+
Size::Logical(LogicalSize::new(self.width as f64, self.height as f64))
89+
}
90+
}
91+
7792
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
7893
#[serde(default)]
7994
struct DesktopSettingsCache {
@@ -82,6 +97,7 @@ struct DesktopSettingsCache {
8297
close_to_tray: bool,
8398
proxy_host: String,
8499
proxy_port: u16,
100+
main_window_size: Option<WindowSizeCache>,
85101
}
86102

87103
impl Default for DesktopSettingsCache {
@@ -92,30 +108,34 @@ impl Default for DesktopSettingsCache {
92108
close_to_tray: true,
93109
proxy_host: DEFAULT_PROXY_HOST.to_string(),
94110
proxy_port: DEFAULT_PROXY_PORT,
111+
main_window_size: None,
95112
}
96113
}
97114
}
98115

99116
impl DesktopSettingsCache {
100117
fn from_app_settings(value: AppSettingsPayload) -> Self {
118+
Self::default().updated_from_app_settings(value)
119+
}
120+
121+
fn updated_from_app_settings(mut self, value: AppSettingsPayload) -> Self {
101122
let defaults = Self::default();
102123
let proxy_host = value.proxy_host.trim();
103124
let proxy_port = if value.proxy_port == 0 {
104125
defaults.proxy_port
105126
} else {
106127
value.proxy_port
107128
};
108-
Self {
109-
launch_at_login: value.launch_at_login,
110-
silent_start: value.silent_start,
111-
close_to_tray: value.close_to_tray,
112-
proxy_host: if proxy_host.is_empty() {
113-
defaults.proxy_host
114-
} else {
115-
proxy_host.to_string()
116-
},
117-
proxy_port,
118-
}
129+
self.launch_at_login = value.launch_at_login;
130+
self.silent_start = value.silent_start;
131+
self.close_to_tray = value.close_to_tray;
132+
self.proxy_host = if proxy_host.is_empty() {
133+
defaults.proxy_host
134+
} else {
135+
proxy_host.to_string()
136+
};
137+
self.proxy_port = proxy_port;
138+
self
119139
}
120140

121141
fn backend_addr(&self) -> String {
@@ -343,6 +363,7 @@ fn main() {
343363
if let Err(err) = sync_launch_agent(cache.launch_at_login) {
344364
eprintln!("sync launch agent failed: {err}");
345365
}
366+
apply_saved_main_window_size(app.handle())?;
346367
spawn_sidecar()?;
347368
wait_for_backend_ready(
348369
&cache.backend_addr(),
@@ -362,23 +383,31 @@ fn main() {
362383
.expect("error while building tauri application")
363384
.run(|app_handle, event| match event {
364385
tauri::RunEvent::WindowEvent { label, event, .. } => {
365-
if label == "main" {
366-
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
367-
match window_close_action(current_settings_cache().close_to_tray) {
368-
WindowCloseAction::MinimizeWindow => {
369-
api.prevent_close();
370-
if let Some(window) = app_handle.get_webview_window("main") {
371-
let _ = window.minimize();
386+
if label == MAIN_WINDOW_LABEL {
387+
match event {
388+
tauri::WindowEvent::CloseRequested { api, .. } => {
389+
match window_close_action(current_settings_cache().close_to_tray) {
390+
WindowCloseAction::MinimizeWindow => {
391+
api.prevent_close();
392+
if let Some(window) =
393+
app_handle.get_webview_window(MAIN_WINDOW_LABEL)
394+
{
395+
let _ = window.minimize();
396+
}
397+
}
398+
WindowCloseAction::ExitApp => {
399+
api.prevent_close();
400+
stop_sidecar_exit_watcher();
401+
stop_resume_recovery_watcher();
402+
shutdown_sidecar();
403+
app_handle.exit(0);
372404
}
373405
}
374-
WindowCloseAction::ExitApp => {
375-
api.prevent_close();
376-
stop_sidecar_exit_watcher();
377-
stop_resume_recovery_watcher();
378-
shutdown_sidecar();
379-
app_handle.exit(0);
380-
}
381406
}
407+
tauri::WindowEvent::Resized(_) => {
408+
let _ = persist_main_window_size_from_window(&app_handle);
409+
}
410+
_ => {}
382411
}
383412
}
384413
}
@@ -810,6 +839,63 @@ fn current_backend_addr() -> String {
810839
current_settings_cache().backend_addr()
811840
}
812841

842+
fn sanitize_main_window_size(width: u32, height: u32) -> Option<WindowSizeCache> {
843+
if width == 0 || height == 0 {
844+
return None;
845+
}
846+
Some(WindowSizeCache {
847+
width: width.max(MAIN_WINDOW_MIN_WIDTH),
848+
height: height.max(MAIN_WINDOW_MIN_HEIGHT),
849+
})
850+
}
851+
852+
fn resolve_main_window_size(size: Option<WindowSizeCache>) -> WindowSizeCache {
853+
size.unwrap_or(WindowSizeCache {
854+
width: MAIN_WINDOW_MIN_WIDTH,
855+
height: MAIN_WINDOW_MIN_HEIGHT,
856+
})
857+
}
858+
859+
fn apply_saved_main_window_size<R: Runtime>(app: &AppHandle<R>) -> Result<(), String> {
860+
let size = resolve_main_window_size(current_settings_cache().main_window_size);
861+
if let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) {
862+
window
863+
.set_size(size.as_tauri_size())
864+
.map_err(|e| format!("apply main window size failed: {e}"))?;
865+
}
866+
Ok(())
867+
}
868+
869+
fn persist_main_window_size(size: WindowSizeCache) -> Result<(), String> {
870+
let mut runtime = DESKTOP_RUNTIME
871+
.lock()
872+
.map_err(|_| "desktop runtime lock poisoned".to_string())?;
873+
if runtime.settings_cache.main_window_size == Some(size.clone()) {
874+
return Ok(());
875+
}
876+
runtime.settings_cache.main_window_size = Some(size);
877+
persist_settings_cache(&runtime.settings_path, &runtime.settings_cache)
878+
}
879+
880+
fn persist_main_window_size_from_window<R: Runtime>(app: &AppHandle<R>) -> Result<(), String> {
881+
let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) else {
882+
return Ok(());
883+
};
884+
let inner_size = window
885+
.inner_size()
886+
.map_err(|e| format!("read main window size failed: {e}"))?;
887+
let scale_factor = window
888+
.scale_factor()
889+
.map_err(|e| format!("read main window scale factor failed: {e}"))?;
890+
let logical_size = inner_size.to_logical::<f64>(scale_factor);
891+
let width = logical_size.width.round().max(0.0) as u32;
892+
let height = logical_size.height.round().max(0.0) as u32;
893+
let Some(size) = sanitize_main_window_size(width, height) else {
894+
return Ok(());
895+
};
896+
persist_main_window_size(size)
897+
}
898+
813899
fn clamp_recent_log_limit(limit: Option<usize>) -> usize {
814900
match limit.unwrap_or(DESKTOP_RECENT_LOG_DEFAULT_LIMIT) {
815901
0 => 1,
@@ -1877,16 +1963,18 @@ mod tests {
18771963
append_recent_desktop_log, build_launch_agent_plist, clamp_recent_log_limit,
18781964
decode_chunked_body, format_timeout_error, format_tray_title, map_backend_io_error,
18791965
parse_account_menu_id, parse_accounts_response, parse_proxy_status_response,
1880-
proxy_menu_enabled_states, should_attempt_sidecar_recovery,
1881-
should_refresh_tray_after_action, should_restart_sidecar_after_exit,
1882-
should_retry_sidecar_request, should_trigger_resume_recovery, sidecar_candidate_paths,
1883-
sidecar_creation_flags, sidecar_request_with_recovery, sidecar_request_with_recovery_hooks,
1966+
proxy_menu_enabled_states, resolve_main_window_size, sanitize_main_window_size,
1967+
should_attempt_sidecar_recovery, should_refresh_tray_after_action,
1968+
should_restart_sidecar_after_exit, should_retry_sidecar_request,
1969+
should_trigger_resume_recovery, sidecar_candidate_paths, sidecar_creation_flags,
1970+
sidecar_request_with_recovery, sidecar_request_with_recovery_hooks,
18841971
sidecar_resource_name, tray_icon_bytes_for_platform, tray_icon_is_template_for_platform,
18851972
update_download_progress, wait_for_backend_ready_with_probe, window_close_action,
1886-
AppSettingsPayload, DesktopLogEntry, DesktopSettingsCache, HttpResponse, UpdateInfoPayload,
1887-
UpdateManagerState, UpdateProgressPayload, UpdateStatePayload, UpdateStatus,
1888-
WindowCloseAction, SIDECAR_MACOS_NAME, SIDECAR_WINDOWS_NAME, TRAY_ICON_COLOR_BYTES,
1889-
TRAY_ICON_TEMPLATE_BYTES, UPDATE_MANAGER,
1973+
AppSettingsPayload, DesktopLogEntry, DesktopSettingsCache, HttpResponse,
1974+
UpdateInfoPayload, UpdateManagerState, UpdateProgressPayload, UpdateStatePayload,
1975+
UpdateStatus, WindowCloseAction, WindowSizeCache, MAIN_WINDOW_MIN_HEIGHT,
1976+
MAIN_WINDOW_MIN_WIDTH, SIDECAR_MACOS_NAME, SIDECAR_WINDOWS_NAME,
1977+
TRAY_ICON_COLOR_BYTES, TRAY_ICON_TEMPLATE_BYTES, UPDATE_MANAGER,
18901978
};
18911979
use std::cell::RefCell;
18921980
use std::collections::VecDeque;
@@ -2052,6 +2140,7 @@ mod tests {
20522140
assert!(cache.close_to_tray);
20532141
assert_eq!(cache.proxy_host, "127.0.0.1");
20542142
assert_eq!(cache.proxy_port, 6789);
2143+
assert_eq!(cache.main_window_size, None);
20552144
assert_eq!(cache.backend_addr(), "127.0.0.1:6789");
20562145
assert_eq!(
20572146
cache.backend_api_base(),
@@ -2073,7 +2162,7 @@ mod tests {
20732162
backup_retention_count: 7,
20742163
};
20752164

2076-
let cache = DesktopSettingsCache::from_app_settings(payload);
2165+
let cache = DesktopSettingsCache::default().updated_from_app_settings(payload);
20772166
assert!(cache.launch_at_login);
20782167
assert!(cache.silent_start);
20792168
assert!(!cache.close_to_tray);
@@ -2096,11 +2185,59 @@ mod tests {
20962185
backup_retention_count: 10,
20972186
};
20982187

2099-
let cache = DesktopSettingsCache::from_app_settings(payload);
2188+
let cache = DesktopSettingsCache::default().updated_from_app_settings(payload);
21002189
assert_eq!(cache.proxy_host, "127.0.0.1");
21012190
assert_eq!(cache.proxy_port, 6789);
21022191
}
21032192

2193+
#[test]
2194+
fn resolved_main_window_size_uses_minimum_dimensions_by_default() {
2195+
let size = resolve_main_window_size(None);
2196+
2197+
assert_eq!(size.width, MAIN_WINDOW_MIN_WIDTH);
2198+
assert_eq!(size.height, MAIN_WINDOW_MIN_HEIGHT);
2199+
}
2200+
2201+
#[test]
2202+
fn desktop_settings_cache_preserves_saved_window_size_when_app_settings_change() {
2203+
let initial = DesktopSettingsCache {
2204+
main_window_size: Some(WindowSizeCache {
2205+
width: 1440,
2206+
height: 900,
2207+
}),
2208+
..DesktopSettingsCache::default()
2209+
};
2210+
let payload = AppSettingsPayload {
2211+
launch_at_login: true,
2212+
silent_start: false,
2213+
close_to_tray: true,
2214+
show_proxy_switch_on_home: true,
2215+
proxy_host: "127.0.0.1".to_string(),
2216+
proxy_port: 6789,
2217+
auto_failover_enabled: false,
2218+
auto_backup_interval_hours: 24,
2219+
backup_retention_count: 10,
2220+
};
2221+
2222+
let updated = initial.updated_from_app_settings(payload);
2223+
2224+
assert_eq!(
2225+
updated.main_window_size,
2226+
Some(WindowSizeCache {
2227+
width: 1440,
2228+
height: 900,
2229+
})
2230+
);
2231+
}
2232+
2233+
#[test]
2234+
fn sanitize_main_window_size_clamps_small_dimensions_to_minimum() {
2235+
let size = sanitize_main_window_size(800, 600).expect("size should be accepted");
2236+
2237+
assert_eq!(size.width, MAIN_WINDOW_MIN_WIDTH);
2238+
assert_eq!(size.height, MAIN_WINDOW_MIN_HEIGHT);
2239+
}
2240+
21042241
#[test]
21052242
fn launch_agent_plist_uses_current_executable() {
21062243
let plist = build_launch_agent_plist(Path::new(

desktop/src-tauri/tauri.conf.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
"windows": [
1414
{
1515
"title": "AI Gate",
16-
"width": 1280,
17-
"height": 820,
16+
"width": 1024,
17+
"height": 700,
1818
"minWidth": 1024,
1919
"minHeight": 700
2020
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Window Size Persistence Design
2+
3+
**Goal:** Desktop app launches at the minimum supported window size by default, and only switches to a remembered size after the user has manually resized the window.
4+
5+
## Decision
6+
7+
- Use the existing desktop settings cache file to persist the main window size.
8+
- Change the built-in default window size to the current minimum size: `1024x700`.
9+
- Keep persisted window size separate from ordinary app settings, but preserve it when app settings are saved.
10+
- Restore the remembered size during desktop startup before normal window interaction.
11+
- Persist logical window size on resize so the remembered size remains stable across DPI changes.
12+
13+
## Why this approach
14+
15+
- It matches the requested behavior exactly: minimum by default, remembered only after user adjustment.
16+
- It reuses the existing desktop persistence path instead of introducing a second storage location.
17+
- Persisting logical size avoids cross-monitor DPI surprises better than raw physical pixels.
18+
- Preserving the saved size during `apply_app_settings` avoids an easy regression where opening Settings would silently reset the remembered window size.

0 commit comments

Comments
 (0)