Files
Genarrative/apps/desktop-shell/src-tauri/src/shell/tray.rs
T
kdletters 67e781c886 加固原生壳分发失败门禁
桌面壳外链和新窗口外链失败改为统一日志记录

桌面壳托盘关闭生命周期和隐藏失败改为统一日志记录

移动壳补齐 iOS Privacy Manifest 与配置门禁

宿主壳方案和项目记忆同步分发约束
2026-06-20 06:23:40 +08:00

211 lines
6.8 KiB
Rust

use crate::shell::lifecycle::{
emit_current_desktop_lifecycle_event, emit_desktop_lifecycle_event,
log_desktop_host_event_result,
};
use tauri::menu::{Menu, MenuItem};
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
use tauri::{Manager, WebviewWindow, WindowEvent};
const DESKTOP_TRAY_ID: &str = "genarrative-desktop-tray";
pub(crate) const TRAY_MENU_SHOW: &str = "show-main-window";
pub(crate) const TRAY_MENU_RELOAD: &str = "reload-main-window";
pub(crate) const TRAY_MENU_QUIT: &str = "quit-desktop-shell";
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum DesktopTrayAction {
ShowMainWindow,
ReloadMainWindow,
QuitApp,
Ignore,
}
#[derive(Debug, PartialEq, Eq)]
enum DesktopWindowCloseAction {
HideToTray,
CloseWindow,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum DesktopSingleInstanceAction {
ShowMainWindow,
}
pub(crate) fn resolve_desktop_tray_menu_action(menu_id: &str) -> DesktopTrayAction {
match menu_id {
TRAY_MENU_SHOW => DesktopTrayAction::ShowMainWindow,
TRAY_MENU_RELOAD => DesktopTrayAction::ReloadMainWindow,
TRAY_MENU_QUIT => DesktopTrayAction::QuitApp,
_ => DesktopTrayAction::Ignore,
}
}
pub(crate) fn resolve_desktop_tray_icon_action(
button: MouseButton,
button_state: MouseButtonState,
) -> DesktopTrayAction {
if button == MouseButton::Left && button_state == MouseButtonState::Up {
DesktopTrayAction::ShowMainWindow
} else {
DesktopTrayAction::Ignore
}
}
fn resolve_desktop_window_close_action(tray_registered: bool) -> DesktopWindowCloseAction {
if tray_registered {
DesktopWindowCloseAction::HideToTray
} else {
DesktopWindowCloseAction::CloseWindow
}
}
pub(crate) fn resolve_desktop_single_instance_action() -> DesktopSingleInstanceAction {
DesktopSingleInstanceAction::ShowMainWindow
}
pub(crate) fn show_main_window(app: &tauri::AppHandle) -> tauri::Result<()> {
if let Some(window) = app.get_webview_window("main") {
window.show()?;
window.unminimize()?;
window.set_focus()?;
emit_current_desktop_lifecycle_event(&window)?;
}
Ok(())
}
pub(crate) fn reload_main_window(app: &tauri::AppHandle) -> tauri::Result<()> {
if let Some(window) = app.get_webview_window("main") {
window.reload()?;
}
Ok(())
}
fn handle_desktop_tray_action(app: &tauri::AppHandle, action: DesktopTrayAction) {
match action {
DesktopTrayAction::ShowMainWindow => {
log_desktop_host_event_result("tray.show", show_main_window(app));
}
DesktopTrayAction::ReloadMainWindow => {
log_desktop_host_event_result("tray.reload", reload_main_window(app));
}
DesktopTrayAction::QuitApp => app.exit(0),
DesktopTrayAction::Ignore => {}
}
}
pub(crate) fn register_desktop_tray(app: &tauri::App) -> tauri::Result<()> {
let show_item = MenuItem::with_id(app, TRAY_MENU_SHOW, "显示主窗口", true, None::<&str>)?;
let reload_item = MenuItem::with_id(app, TRAY_MENU_RELOAD, "刷新", true, None::<&str>)?;
let quit_item = MenuItem::with_id(app, TRAY_MENU_QUIT, "退出", true, None::<&str>)?;
let tray_menu = Menu::with_items(app, &[&show_item, &reload_item, &quit_item])?;
let mut tray_builder = TrayIconBuilder::with_id(DESKTOP_TRAY_ID)
.menu(&tray_menu)
.tooltip("Genarrative")
.show_menu_on_left_click(false)
.on_menu_event(|app, event| {
let menu_id = event.id().0.as_str();
handle_desktop_tray_action(app, resolve_desktop_tray_menu_action(menu_id));
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click {
button,
button_state,
..
} = event
{
handle_desktop_tray_action(
tray.app_handle(),
resolve_desktop_tray_icon_action(button, button_state),
);
}
});
if let Some(icon) = app.default_window_icon().cloned() {
tray_builder = tray_builder.icon(icon);
}
tray_builder.build(app)?;
Ok(())
}
pub(crate) fn register_desktop_window_close_events(window: &WebviewWindow, tray_registered: bool) {
let close_window = window.clone();
window.on_window_event(move |event| {
if let WindowEvent::CloseRequested { api, .. } = event {
if resolve_desktop_window_close_action(tray_registered)
== DesktopWindowCloseAction::HideToTray
{
api.prevent_close();
// 中文注释:隐藏 WebView 前先通知 H5 暂停游戏循环和音频,避免隐藏后脚本执行被平台挂起。
log_desktop_host_event_result(
"tray.close.lifecycle",
emit_desktop_lifecycle_event(&close_window, "background", false, "hidden"),
);
log_desktop_host_event_result("tray.close.hide", close_window.hide());
}
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn desktop_tray_menu_ids_map_to_real_window_actions() {
assert_eq!(
resolve_desktop_tray_menu_action(TRAY_MENU_SHOW),
DesktopTrayAction::ShowMainWindow
);
assert_eq!(
resolve_desktop_tray_menu_action(TRAY_MENU_RELOAD),
DesktopTrayAction::ReloadMainWindow
);
assert_eq!(
resolve_desktop_tray_menu_action(TRAY_MENU_QUIT),
DesktopTrayAction::QuitApp
);
assert_eq!(
resolve_desktop_tray_menu_action("unknown"),
DesktopTrayAction::Ignore
);
}
#[test]
fn desktop_tray_left_click_restores_main_window_only_on_release() {
assert_eq!(
resolve_desktop_tray_icon_action(MouseButton::Left, MouseButtonState::Up),
DesktopTrayAction::ShowMainWindow
);
assert_eq!(
resolve_desktop_tray_icon_action(MouseButton::Left, MouseButtonState::Down),
DesktopTrayAction::Ignore
);
assert_eq!(
resolve_desktop_tray_icon_action(MouseButton::Right, MouseButtonState::Up),
DesktopTrayAction::Ignore
);
}
#[test]
fn desktop_close_hides_to_tray_only_when_tray_is_registered() {
assert_eq!(
resolve_desktop_window_close_action(true),
DesktopWindowCloseAction::HideToTray
);
assert_eq!(
resolve_desktop_window_close_action(false),
DesktopWindowCloseAction::CloseWindow
);
}
#[test]
fn desktop_single_instance_only_restores_existing_window() {
assert_eq!(
resolve_desktop_single_instance_action(),
DesktopSingleInstanceAction::ShowMainWindow
);
}
}