@@ -786,6 +786,137 @@ fn signal_frontend_ready(
786786 Ok ( ( ) )
787787}
788788
789+ #[ tauri:: command]
790+ fn quit_app ( app : AppHandle ) {
791+ app. exit ( 0 ) ;
792+ }
793+
794+ #[ cfg( target_os = "windows" ) ]
795+ fn force_show_foreground_window ( window : & tauri:: WebviewWindow ) {
796+ use windows_sys:: Win32 :: Foundation :: HWND ;
797+ use windows_sys:: Win32 :: UI :: WindowsAndMessaging :: {
798+ BringWindowToTop , IsIconic , SetForegroundWindow , SetWindowPos , ShowWindow , HWND_NOTOPMOST ,
799+ HWND_TOPMOST , SWP_NOACTIVATE , SWP_NOMOVE , SWP_NOSIZE , SW_RESTORE , SW_SHOW ,
800+ } ;
801+
802+ let Ok ( hwnd) = window. hwnd ( ) else {
803+ return ;
804+ } ;
805+ let raw = hwnd. 0 as HWND ;
806+ if raw. is_null ( ) {
807+ return ;
808+ }
809+
810+ unsafe {
811+ if IsIconic ( raw) != 0 {
812+ ShowWindow ( raw, SW_RESTORE ) ;
813+ } else {
814+ ShowWindow ( raw, SW_SHOW ) ;
815+ }
816+
817+ SetWindowPos (
818+ raw,
819+ HWND_TOPMOST ,
820+ 0 ,
821+ 0 ,
822+ 0 ,
823+ 0 ,
824+ SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE ,
825+ ) ;
826+ SetWindowPos (
827+ raw,
828+ HWND_NOTOPMOST ,
829+ 0 ,
830+ 0 ,
831+ 0 ,
832+ 0 ,
833+ SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE ,
834+ ) ;
835+ BringWindowToTop ( raw) ;
836+ SetForegroundWindow ( raw) ;
837+ }
838+
839+ reapply_chrome_styles ( raw) ;
840+ }
841+
842+ fn show_and_focus_main_window ( app : & AppHandle ) {
843+ let win = app
844+ . get_webview_window ( "main" )
845+ . or_else ( || app. webview_windows ( ) . into_values ( ) . next ( ) )
846+ . or_else ( || app. try_state :: < MainWindowHandle > ( ) . map ( |h| h. inner ( ) . 0 . clone ( ) ) ) ;
847+ let Some ( win) = win else {
848+ tracing:: warn!( "[sen-desktop] tray reveal: no main window handle available" ) ;
849+ return ;
850+ } ;
851+
852+ #[ cfg( target_os = "windows" ) ]
853+ force_show_foreground_window ( & win) ;
854+
855+ let win_main = win. clone ( ) ;
856+ let dispatched = app. run_on_main_thread ( move || {
857+ if win_main. is_minimized ( ) . unwrap_or ( false ) {
858+ let _ = win_main. unminimize ( ) ;
859+ }
860+ if let Err ( err) = win_main. show ( ) {
861+ tracing:: warn!( "[sen-desktop] tray reveal show() failed: {err}" ) ;
862+ }
863+ let _ = win_main. set_focus ( ) ;
864+ #[ cfg( target_os = "windows" ) ]
865+ force_show_foreground_window ( & win_main) ;
866+ } ) ;
867+ if let Err ( err) = dispatched {
868+ tracing:: warn!( "[sen-desktop] tray reveal run_on_main_thread failed: {err}" ) ;
869+ let _ = win. show ( ) ;
870+ let _ = win. set_focus ( ) ;
871+ }
872+ }
873+
874+ const TRAY_QUIT_EVENT : & str = "tray://quit-requested" ;
875+
876+ fn setup_system_tray ( app : & AppHandle ) -> tauri:: Result < ( ) > {
877+ use tauri:: menu:: { Menu , MenuItem } ;
878+ use tauri:: tray:: { MouseButton , MouseButtonState , TrayIconBuilder , TrayIconEvent } ;
879+
880+ let show_item = MenuItem :: with_id ( app, "tray_show" , "显示主窗口" , true , None :: < & str > ) ?;
881+ let quit_item = MenuItem :: with_id ( app, "tray_quit" , "退出" , true , None :: < & str > ) ?;
882+ let menu = Menu :: with_items ( app, & [ & show_item, & quit_item] ) ?;
883+
884+ let mut builder = TrayIconBuilder :: with_id ( "sen-main-tray" )
885+ . tooltip ( "SenWeaverCoding" )
886+ . menu ( & menu)
887+ . show_menu_on_left_click ( false )
888+ . on_menu_event ( |app, event| match event. id . as_ref ( ) {
889+ "tray_show" => show_and_focus_main_window ( app) ,
890+ "tray_quit" => {
891+ if let Err ( err) = app. emit ( TRAY_QUIT_EVENT , ( ) ) {
892+ tracing:: warn!( "[sen-desktop] emit {TRAY_QUIT_EVENT} failed: {err}" ) ;
893+ }
894+ }
895+ _ => { }
896+ } )
897+ . on_tray_icon_event ( |tray, event| match event {
898+ TrayIconEvent :: Click {
899+ button : MouseButton :: Left ,
900+ button_state : MouseButtonState :: Up ,
901+ ..
902+ }
903+ | TrayIconEvent :: DoubleClick {
904+ button : MouseButton :: Left ,
905+ ..
906+ } => {
907+ show_and_focus_main_window ( tray. app_handle ( ) ) ;
908+ }
909+ _ => { }
910+ } ) ;
911+
912+ if let Some ( icon) = app. default_window_icon ( ) {
913+ builder = builder. icon ( icon. clone ( ) ) ;
914+ }
915+
916+ builder. build ( app) ?;
917+ Ok ( ( ) )
918+ }
919+
789920const FRONTEND_READY_TIMEOUT_MS : u64 = 60_000 ;
790921
791922fn show_main_window_now ( window : & tauri:: WebviewWindow ) {
@@ -1132,6 +1263,9 @@ pub fn run() {
11321263 . manage ( TerminalState :: default ( ) )
11331264 . manage ( DockSharedState :: new ( ) )
11341265 . register_uri_scheme_protocol ( "senbridge" , browser_dock:: senbridge_protocol_handler)
1266+ . plugin ( tauri_plugin_single_instance:: init ( |app, _argv, _cwd| {
1267+ show_and_focus_main_window ( app) ;
1268+ } ) )
11351269 . plugin ( tauri_plugin_shell:: init ( ) )
11361270 . plugin ( tauri_plugin_dialog:: init ( ) )
11371271 . plugin ( tauri_plugin_process:: init ( ) )
@@ -1144,6 +1278,7 @@ pub fn run() {
11441278 open_log_dir,
11451279 prepare_for_update_install,
11461280 signal_frontend_ready,
1281+ quit_app,
11471282 reveal_in_explorer,
11481283 read_local_image_data_url,
11491284 curator_render_docx_with_diagrams,
@@ -1201,6 +1336,10 @@ pub fn run() {
12011336 browser_dock:: install_into ( app. handle ( ) ) ;
12021337 fetch_worker:: install_into ( app. handle ( ) ) ;
12031338
1339+ if let Err ( err) = setup_system_tray ( app. handle ( ) ) {
1340+ tracing:: warn!( "[sen-desktop] system tray setup failed: {err}" ) ;
1341+ }
1342+
12041343 let handle = app. handle ( ) . clone ( ) ;
12051344
12061345 let server_state = app. state :: < ServerState > ( ) . inner ( ) . clone ( ) ;
@@ -1278,12 +1417,12 @@ pub fn run() {
12781417
12791418 app. run ( |app_handle, event| match event {
12801419 RunEvent :: ExitRequested { .. } => {
1281-
1420+ let _ = app_handle . remove_tray_by_id ( "sen-main-tray" ) ;
12821421 process_lifetime:: run_full_shutdown ( app_handle, Duration :: from_secs ( 8 ) ) ;
12831422 kill_gateway_child ( ) ;
12841423 }
12851424 RunEvent :: Exit => {
1286-
1425+ let _ = app_handle . remove_tray_by_id ( "sen-main-tray" ) ;
12871426 process_lifetime:: run_full_shutdown ( app_handle, Duration :: from_secs ( 2 ) ) ;
12881427 kill_gateway_child ( ) ;
12891428 }
0 commit comments