@@ -20,7 +20,7 @@ use std::time::{Duration, Instant};
2020use tauri:: image:: Image ;
2121use tauri:: menu:: { Menu , MenuBuilder , MenuItemBuilder } ;
2222use tauri:: tray:: TrayIconBuilder ;
23- use tauri:: { AppHandle , Emitter , Manager , Runtime } ;
23+ use tauri:: { AppHandle , Emitter , LogicalSize , Manager , Runtime , Size } ;
2424use tauri_plugin_updater:: { Update , UpdaterExt } ;
2525
2626#[ cfg( windows) ]
@@ -44,6 +44,9 @@ static UPDATE_MANAGER: Lazy<Mutex<UpdateManagerState>> =
4444const DEFAULT_PROXY_HOST : & str = "127.0.0.1" ;
4545const DEFAULT_PROXY_PORT : u16 = 6789 ;
4646const 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 ;
4750const LAUNCH_AGENT_LABEL : & str = "com.aigate.desktop" ;
4851const TRAY_ID : & str = "aigate-tray" ;
4952const MENU_OPEN_MAIN : & str = "open-main" ;
@@ -74,6 +77,18 @@ const UPDATER_PUBKEY_BASE64: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1Ymx
7477#[ cfg( windows) ]
7578const 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 ) ]
7994struct 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
87103impl 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
99116impl 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+
813899fn 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 (
0 commit comments