Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions apps/src-tauri/src/app_shell/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}
_ => {}
}
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions apps/src-tauri/src/app_shell/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
27 changes: 4 additions & 23 deletions apps/src-tauri/src/app_shell/tray.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use rfd::{MessageButtons, MessageDialog, MessageLevel};
use tauri::menu::{Menu, MenuItem};
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};

Expand All @@ -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
Expand All @@ -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() {
Expand Down
67 changes: 60 additions & 7 deletions apps/src-tauri/src/app_shell/window.rs
Original file line number Diff line number Diff line change
@@ -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`
///
Expand All @@ -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) {
Expand Down
11 changes: 5 additions & 6 deletions apps/src-tauri/src/commands/system.rs
Original file line number Diff line number Diff line change
@@ -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,
},
Expand Down Expand Up @@ -65,8 +65,7 @@ pub fn app_window_unsaved_draft_sections_set(sections: Vec<String>) -> 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)
}
15 changes: 10 additions & 5 deletions apps/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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();
Expand Down