Files
Genarrative/apps/desktop-shell/src-tauri/src/shell/tray.rs
T
kdletters ec36300d92 桌面壳接入应用菜单
新增 Tauri 应用菜单并复用显示、刷新和退出窗口动作

收紧桌面壳菜单模块和结构门禁

同步宿主壳方案文档和团队决策记录
2026-06-18 19:42:17 +08:00

201 lines
6.1 KiB
Rust

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()?;
}
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 => {
let _ = show_main_window(app);
}
DesktopTrayAction::ReloadMainWindow => {
let _ = 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();
let _ = 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
);
}
}