diff --git a/apps/src-tauri/src/app_shell/lifecycle.rs b/apps/src-tauri/src/app_shell/lifecycle.rs index 87ac11233..f3f9395e5 100644 --- a/apps/src-tauri/src/app_shell/lifecycle.rs +++ b/apps/src-tauri/src/app_shell/lifecycle.rs @@ -18,7 +18,7 @@ use super::state::{ TRAY_AVAILABLE, }; #[cfg(target_os = "macos")] -use super::window::show_main_window; +use super::window::request_show_main_window; use super::window::{hide_tray_preview_window, MAIN_WINDOW_LABEL, TRAY_PREVIEW_WINDOW_LABEL}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -49,7 +49,7 @@ fn resolve_main_window_close_mode( if !close_to_tray_on_close || !tray_available { return MainWindowCloseMode::AllowWindowClose; } - if lightweight_tray_close { + if cfg!(target_os = "windows") || lightweight_tray_close { return MainWindowCloseMode::CloseForLightweightTray; } MainWindowCloseMode::HideToTray @@ -225,7 +225,9 @@ pub(crate) fn handle_run_event(app: &tauri::AppHandle, event: &tauri::RunEvent) } #[cfg(target_os = "macos")] tauri::RunEvent::Reopen { .. } => { - show_main_window(app); + if let Err(err) = request_show_main_window(app) { + log::warn!("request show main window from reopen failed: {}", err); + } } _ => {} } @@ -259,10 +261,16 @@ mod tests { resolve_main_window_close_mode(true, false, false), MainWindowCloseMode::AllowWindowClose ); + #[cfg(not(target_os = "windows"))] assert_eq!( resolve_main_window_close_mode(true, true, false), MainWindowCloseMode::HideToTray ); + #[cfg(target_os = "windows")] + assert_eq!( + resolve_main_window_close_mode(true, true, false), + MainWindowCloseMode::CloseForLightweightTray + ); assert_eq!( resolve_main_window_close_mode(true, true, true), MainWindowCloseMode::CloseForLightweightTray diff --git a/apps/src-tauri/src/app_shell/mod.rs b/apps/src-tauri/src/app_shell/mod.rs index abd251902..cc27b4162 100644 --- a/apps/src-tauri/src/app_shell/mod.rs +++ b/apps/src-tauri/src/app_shell/mod.rs @@ -13,5 +13,5 @@ pub(crate) use state::{ prepare_for_forced_app_exit, set_unsaved_settings_draft_sections, CLOSE_TO_TRAY_ON_CLOSE, KEEP_ALIVE_FOR_LIGHTWEIGHT_CLOSE, LIGHTWEIGHT_MODE_ON_CLOSE_TO_TRAY, TRAY_AVAILABLE, }; -pub(crate) use tray::{notify_existing_instance_focused, setup_tray}; -pub(crate) use window::show_main_window; +pub(crate) use tray::setup_tray; +pub(crate) use window::request_show_main_window; diff --git a/apps/src-tauri/src/app_shell/tray.rs b/apps/src-tauri/src/app_shell/tray.rs index 606209759..8d536e5e9 100644 --- a/apps/src-tauri/src/app_shell/tray.rs +++ b/apps/src-tauri/src/app_shell/tray.rs @@ -1,4 +1,3 @@ -use rfd::{MessageButtons, MessageDialog, MessageLevel}; use tauri::menu::{Menu, MenuItem}; use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}; @@ -9,31 +8,11 @@ use super::state::{ has_unsaved_settings_draft_sections, mark_skip_next_unsaved_settings_exit_confirm, APP_EXIT_REQUESTED, KEEP_ALIVE_FOR_LIGHTWEIGHT_CLOSE, TRAY_AVAILABLE, }; -use super::window::{show_main_window, toggle_tray_preview_window}; +use super::window::{request_show_main_window, toggle_tray_preview_window}; const TRAY_MENU_SHOW_MAIN: &str = "tray_show_main"; const TRAY_MENU_QUIT_APP: &str = "tray_quit_app"; -/// 函数 `notify_existing_instance_focused` -/// -/// 作者: gaohongshun -/// -/// 时间: 2026-04-02 -/// -/// # 参数 -/// - crate: 参数 crate -/// -/// # 返回 -/// 无 -pub(crate) fn notify_existing_instance_focused() { - let _ = MessageDialog::new() - .set_title("CodexManager") - .set_description("CodexManager 已在运行,已切换到现有窗口。") - .set_level(MessageLevel::Info) - .set_buttons(MessageButtons::Ok) - .show(); -} - /// 函数 `setup_tray` /// /// 作者: gaohongshun @@ -55,7 +34,9 @@ pub(crate) fn setup_tray(app: &tauri::AppHandle) -> Result<(), tauri::Error> { .show_menu_on_left_click(false) .on_menu_event(|app, event| match event.id().as_ref() { TRAY_MENU_SHOW_MAIN => { - show_main_window(app); + if let Err(err) = request_show_main_window(app) { + log::warn!("request show main window from tray failed: {}", err); + } } TRAY_MENU_QUIT_APP => { if has_unsaved_settings_draft_sections() { diff --git a/apps/src-tauri/src/app_shell/window.rs b/apps/src-tauri/src/app_shell/window.rs index ec39e5a6f..256f1c56d 100644 --- a/apps/src-tauri/src/app_shell/window.rs +++ b/apps/src-tauri/src/app_shell/window.rs @@ -1,15 +1,18 @@ +use std::sync::atomic::{AtomicBool, Ordering}; + use tauri::webview::Color; use tauri::window::{Effect, EffectState, EffectsBuilder}; use tauri::Manager; use tauri::{PhysicalPosition, PhysicalRect, Rect, WebviewUrl, WebviewWindowBuilder}; -use super::state::KEEP_ALIVE_FOR_LIGHTWEIGHT_CLOSE; +use super::state::{APP_EXIT_REQUESTED, KEEP_ALIVE_FOR_LIGHTWEIGHT_CLOSE}; pub(crate) const MAIN_WINDOW_LABEL: &str = "main"; pub(crate) const TRAY_PREVIEW_WINDOW_LABEL: &str = "tray-preview"; const TRAY_PREVIEW_WIDTH: f64 = 360.0; const TRAY_PREVIEW_HEIGHT: f64 = 390.0; const TRAY_PREVIEW_MARGIN: f64 = 8.0; +static SHOW_MAIN_WINDOW_PENDING: AtomicBool = AtomicBool::new(false); /// 函数 `show_main_window` /// @@ -22,18 +25,68 @@ const TRAY_PREVIEW_MARGIN: f64 = 8.0; /// /// # 返回 /// 无 -pub(crate) fn show_main_window(app: &tauri::AppHandle) { +fn show_main_window(app: &tauri::AppHandle) -> bool { + if APP_EXIT_REQUESTED.load(Ordering::Relaxed) { + log::info!("show main window skipped because app exit is already requested"); + return false; + } + log::info!("show main window requested"); hide_tray_preview_window(app); - KEEP_ALIVE_FOR_LIGHTWEIGHT_CLOSE.store(false, std::sync::atomic::Ordering::Relaxed); + KEEP_ALIVE_FOR_LIGHTWEIGHT_CLOSE.store(false, Ordering::Relaxed); let Some(window) = ensure_main_window(app) else { - return; + return false; }; + if let Err(err) = window.unminimize() { + log::debug!("unminimize main window before show skipped: {}", err); + } if let Err(err) = window.show() { log::warn!("show main window failed: {}", err); - return; + return false; } - let _ = window.unminimize(); - let _ = window.set_focus(); + if let Err(err) = window.unminimize() { + log::warn!("unminimize main window after show failed: {}", err); + } + if let Err(err) = window.set_focus() { + log::warn!("focus main window failed: {}", err); + } + log::info!("show main window completed"); + true +} + +pub(crate) fn request_show_main_window(app: &tauri::AppHandle) -> Result<(), String> { + if APP_EXIT_REQUESTED.load(Ordering::Relaxed) { + return Err("app is exiting; show main window request skipped".to_string()); + } + if SHOW_MAIN_WINDOW_PENDING.swap(true, Ordering::AcqRel) { + log::debug!("show main window request coalesced because one is already pending"); + return Ok(()); + } + + let app = app.clone(); + std::thread::spawn(move || { + if APP_EXIT_REQUESTED.load(Ordering::Relaxed) { + SHOW_MAIN_WINDOW_PENDING.store(false, Ordering::Release); + return; + } + let app_for_show = app.clone(); + if let Err(err) = app.run_on_main_thread(move || { + if APP_EXIT_REQUESTED.load(Ordering::Relaxed) { + log::info!("show main window skipped on main thread because app is exiting"); + SHOW_MAIN_WINDOW_PENDING.store(false, Ordering::Release); + return; + } + let shown = show_main_window(&app_for_show); + if !shown { + log::warn!("show main window request completed without showing a window"); + } + SHOW_MAIN_WINDOW_PENDING.store(false, Ordering::Release); + }) { + log::warn!("schedule show main window on main thread failed: {}", err); + KEEP_ALIVE_FOR_LIGHTWEIGHT_CLOSE.store(false, Ordering::Relaxed); + SHOW_MAIN_WINDOW_PENDING.store(false, Ordering::Release); + } + }); + Ok(()) } pub(crate) fn hide_tray_preview_window(app: &tauri::AppHandle) { diff --git a/apps/src-tauri/src/commands/system.rs b/apps/src-tauri/src/commands/system.rs index ce7a46029..5c0c47334 100644 --- a/apps/src-tauri/src/commands/system.rs +++ b/apps/src-tauri/src/commands/system.rs @@ -1,5 +1,5 @@ use crate::{ - app_shell::{set_unsaved_settings_draft_sections, show_main_window}, + app_shell::{request_show_main_window, set_unsaved_settings_draft_sections}, commands::shared::{ open_external_url_blocking, open_in_browser_blocking, open_in_file_manager_blocking, }, @@ -65,8 +65,7 @@ pub fn app_window_unsaved_draft_sections_set(sections: Vec) -> Result<() Ok(()) } -#[tauri::command] -pub fn app_show_main_window(app: tauri::AppHandle) -> Result<(), String> { - show_main_window(&app); - Ok(()) -} +#[tauri::command] +pub fn app_show_main_window(app: tauri::AppHandle) -> Result<(), String> { + request_show_main_window(&app) +} diff --git a/apps/src-tauri/src/lib.rs b/apps/src-tauri/src/lib.rs index 54761101a..74b386d79 100644 --- a/apps/src-tauri/src/lib.rs +++ b/apps/src-tauri/src/lib.rs @@ -8,9 +8,8 @@ mod rpc_client; mod service_runtime; use app_shell::{ - handle_main_window_event, handle_run_event, load_env_from_exe_dir, - notify_existing_instance_focused, setup_tray, show_main_window, sync_startup_window_state, - CLOSE_TO_TRAY_ON_CLOSE, TRAY_AVAILABLE, + handle_main_window_event, handle_run_event, load_env_from_exe_dir, request_show_main_window, + setup_tray, sync_startup_window_state, CLOSE_TO_TRAY_ON_CLOSE, TRAY_AVAILABLE, }; const USAGE_REFRESH_COMPLETED_EVENT: &str = "usage-refresh-completed"; @@ -43,8 +42,14 @@ pub fn run() { args, cwd ); - show_main_window(app); - notify_existing_instance_focused(); + match request_show_main_window(app) { + Ok(()) => { + log::info!("secondary instance focus request queued without blocking dialog"); + } + Err(err) => { + log::warn!("secondary instance focus request skipped: {}", err); + } + } })) .setup(|app| { load_env_from_exe_dir();