Add: tray for tauri

This commit is contained in:
DaZuo0122
2026-02-13 23:10:01 +08:00
parent e5e123cc84
commit 907974e61f
6 changed files with 288 additions and 8 deletions

View File

@@ -2,18 +2,29 @@
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use base64::Engine;
use sprimo_protocol::v1::FrontendCommand;
use sprimo_platform::{create_adapter, PlatformAdapter};
use sprimo_protocol::v1::{FrontendState, FrontendStateSnapshot};
use sprimo_runtime_core::{RuntimeCore, RuntimeCoreError};
use sprimo_sprite::{load_manifest, resolve_pack_path, AnimationDefinition};
use std::sync::Arc;
use tauri::{Emitter, Manager};
use tauri::menu::{CheckMenuItem, Menu, MenuItem};
use tauri::tray::TrayIconBuilder;
use tauri::{AppHandle, Emitter, Manager, Wry};
use thiserror::Error;
use tokio::runtime::Runtime;
use tracing::warn;
const APP_NAME: &str = "sprimo";
const DEFAULT_PACK: &str = "default";
const MAIN_WINDOW_LABEL: &str = "main";
const TRAY_ID: &str = "main";
const MENU_ID_TOGGLE_VISIBILITY: &str = "toggle_visibility";
const MENU_ID_TOGGLE_ALWAYS_ON_TOP: &str = "toggle_always_on_top";
const MENU_ID_TOGGLE_DEBUG_OVERLAY: &str = "toggle_debug_overlay";
const MENU_ID_QUIT: &str = "quit";
const EVENT_RUNTIME_SNAPSHOT: &str = "runtime:snapshot";
const EVENT_RUNTIME_DEBUG_OVERLAY_VISIBLE: &str = "runtime:debug-overlay-visible";
#[derive(Debug, Clone, serde::Serialize)]
struct UiAnimationClip {
@@ -65,6 +76,13 @@ struct AppState {
runtime: Arc<Runtime>,
}
#[derive(Clone)]
struct TrayMenuState {
toggle_visibility: MenuItem<Wry>,
toggle_always_on_top: CheckMenuItem<Wry>,
toggle_debug_overlay: CheckMenuItem<Wry>,
}
#[tauri::command]
fn current_state(state: tauri::State<'_, AppState>) -> Result<UiSnapshot, String> {
let snapshot = state
@@ -131,6 +149,7 @@ fn debug_overlay_visible(state: tauri::State<'_, AppState>) -> Result<bool, Stri
#[tauri::command]
fn set_debug_overlay_visible(
app_handle: tauri::AppHandle,
state: tauri::State<'_, AppState>,
visible: bool,
) -> Result<bool, String> {
@@ -138,6 +157,7 @@ fn set_debug_overlay_visible(
.runtime_core
.set_frontend_debug_overlay_visible(visible)
.map_err(|err| err.to_string())?;
let _ = emit_debug_overlay_visibility(state.runtime_core.as_ref(), &app_handle);
Ok(visible)
}
@@ -173,13 +193,17 @@ fn main() -> Result<(), AppError> {
let runtime = Arc::clone(&app_state.runtime);
let app_handle = app.handle().clone();
let tray_state = setup_tray(&app_handle, &runtime_core)?;
if let Ok(snapshot) = runtime_core.snapshot().read() {
let _ = app_handle.emit("runtime:snapshot", to_ui_snapshot(&snapshot));
let _ = app_handle.emit(EVENT_RUNTIME_SNAPSHOT, to_ui_snapshot(&snapshot));
}
let _ = emit_debug_overlay_visibility(runtime_core.as_ref(), &app_handle);
let _ = refresh_tray_menu_state(runtime_core.as_ref(), &tray_state);
let command_rx = runtime_core.command_receiver();
let runtime_core_for_commands = Arc::clone(&runtime_core);
let app_handle_for_commands = app_handle.clone();
let tray_state_for_commands = tray_state.clone();
runtime.spawn(async move {
loop {
let next = {
@@ -203,7 +227,11 @@ fn main() -> Result<(), AppError> {
}
};
if let Some(value) = payload {
let _ = app_handle_for_commands.emit("runtime:snapshot", value);
let _ = app_handle_for_commands.emit(EVENT_RUNTIME_SNAPSHOT, value);
let _ = refresh_tray_menu_state(
runtime_core_for_commands.as_ref(),
&tray_state_for_commands,
);
}
}
});
@@ -211,6 +239,7 @@ fn main() -> Result<(), AppError> {
if let Some(window) = app.get_webview_window("main") {
let runtime_core = Arc::clone(&runtime_core);
let app_handle = app_handle.clone();
let tray_state_for_window = tray_state.clone();
window.on_window_event(move |event| {
if let tauri::WindowEvent::Moved(position) = event {
let command = sprimo_protocol::v1::FrontendCommand::SetTransform {
@@ -222,13 +251,34 @@ fn main() -> Result<(), AppError> {
};
if runtime_core.apply_command(&command).is_ok() {
if let Ok(snapshot) = runtime_core.snapshot().read() {
let _ = app_handle.emit("runtime:snapshot", to_ui_snapshot(&snapshot));
let _ = app_handle.emit(
EVENT_RUNTIME_SNAPSHOT,
to_ui_snapshot(&snapshot),
);
}
let _ = refresh_tray_menu_state(
runtime_core.as_ref(),
&tray_state_for_window,
);
}
}
});
}
let runtime_core_for_menu = Arc::clone(&runtime_core);
let app_handle_for_menu = app_handle.clone();
let tray_state_for_menu = tray_state.clone();
app.on_menu_event(move |_app, event| {
if let Err(err) = handle_menu_event(
runtime_core_for_menu.as_ref(),
&app_handle_for_menu,
&tray_state_for_menu,
event.id().as_ref(),
) {
warn!(%err, "tray/menu action failed");
}
});
let _ = app;
Ok(())
})
@@ -248,6 +298,211 @@ fn to_ui_snapshot(snapshot: &FrontendStateSnapshot) -> UiSnapshot {
}
}
fn setup_tray(
app_handle: &AppHandle<Wry>,
runtime_core: &RuntimeCore,
) -> Result<TrayMenuState, tauri::Error> {
let snapshot = runtime_core
.snapshot()
.read()
.map_err(|_| tauri::Error::AssetNotFound("snapshot lock poisoned".to_string()))?
.clone();
let debug_overlay_visible = runtime_core
.frontend_debug_overlay_visible()
.map_err(|err| tauri::Error::AssetNotFound(err.to_string()))?;
let toggle_visibility = MenuItem::with_id(
app_handle,
MENU_ID_TOGGLE_VISIBILITY,
visibility_menu_title(snapshot.flags.visible),
true,
None::<&str>,
)?;
let toggle_always_on_top = CheckMenuItem::with_id(
app_handle,
MENU_ID_TOGGLE_ALWAYS_ON_TOP,
"Always on top",
true,
snapshot.flags.always_on_top,
None::<&str>,
)?;
let toggle_debug_overlay = CheckMenuItem::with_id(
app_handle,
MENU_ID_TOGGLE_DEBUG_OVERLAY,
"Debug overlay",
true,
debug_overlay_visible,
None::<&str>,
)?;
let quit = MenuItem::with_id(app_handle, MENU_ID_QUIT, "Quit", true, None::<&str>)?;
let menu = Menu::with_items(
app_handle,
&[
&toggle_visibility,
&toggle_always_on_top,
&toggle_debug_overlay,
&quit,
],
)?;
let mut builder = TrayIconBuilder::with_id(TRAY_ID).menu(&menu);
if let Some(icon) = app_handle.default_window_icon().cloned() {
builder = builder.icon(icon);
}
builder
.tooltip("sprimo-tauri")
.show_menu_on_left_click(true)
.build(app_handle)?;
Ok(TrayMenuState {
toggle_visibility,
toggle_always_on_top,
toggle_debug_overlay,
})
}
fn handle_menu_event(
runtime_core: &RuntimeCore,
app_handle: &AppHandle<Wry>,
tray_state: &TrayMenuState,
menu_id: &str,
) -> Result<(), String> {
match menu_id {
MENU_ID_TOGGLE_VISIBILITY => {
let current = runtime_core
.snapshot()
.read()
.map_err(|_| "snapshot lock poisoned".to_string())?
.flags
.visible;
let next = !current;
runtime_core
.apply_command(&FrontendCommand::SetFlags {
click_through: None,
always_on_top: None,
visible: Some(next),
})
.map_err(|err| err.to_string())?;
if let Some(window) = app_handle.get_webview_window(MAIN_WINDOW_LABEL) {
if next {
window.show().map_err(|err| err.to_string())?;
window.set_focus().map_err(|err| err.to_string())?;
} else {
window.hide().map_err(|err| err.to_string())?;
}
}
}
MENU_ID_TOGGLE_ALWAYS_ON_TOP => {
let current = runtime_core
.snapshot()
.read()
.map_err(|_| "snapshot lock poisoned".to_string())?
.flags
.always_on_top;
let next = !current;
runtime_core
.apply_command(&FrontendCommand::SetFlags {
click_through: None,
always_on_top: Some(next),
visible: None,
})
.map_err(|err| err.to_string())?;
if let Some(window) = app_handle.get_webview_window(MAIN_WINDOW_LABEL) {
window.set_always_on_top(next).map_err(|err| err.to_string())?;
}
}
MENU_ID_TOGGLE_DEBUG_OVERLAY => {
let current = runtime_core
.frontend_debug_overlay_visible()
.map_err(|err| err.to_string())?;
let next = !current;
runtime_core
.set_frontend_debug_overlay_visible(next)
.map_err(|err| err.to_string())?;
emit_debug_overlay_visibility(runtime_core, app_handle).map_err(|err| err.to_string())?;
}
MENU_ID_QUIT => {
persist_current_ui_flags(runtime_core)?;
app_handle.exit(0);
}
_ => return Ok(()),
}
if let Ok(snapshot) = runtime_core.snapshot().read() {
let _ = app_handle.emit(EVENT_RUNTIME_SNAPSHOT, to_ui_snapshot(&snapshot));
}
let _ = emit_debug_overlay_visibility(runtime_core, app_handle);
let _ = refresh_tray_menu_state(runtime_core, tray_state);
Ok(())
}
fn persist_current_ui_flags(runtime_core: &RuntimeCore) -> Result<(), String> {
let snapshot = runtime_core
.snapshot()
.read()
.map_err(|_| "snapshot lock poisoned".to_string())?
.clone();
runtime_core
.apply_command(&FrontendCommand::SetFlags {
click_through: None,
always_on_top: Some(snapshot.flags.always_on_top),
visible: Some(snapshot.flags.visible),
})
.map_err(|err| err.to_string())?;
let debug_overlay = runtime_core
.frontend_debug_overlay_visible()
.map_err(|err| err.to_string())?;
runtime_core
.set_frontend_debug_overlay_visible(debug_overlay)
.map_err(|err| err.to_string())
}
fn emit_debug_overlay_visibility(
runtime_core: &RuntimeCore,
app_handle: &AppHandle<Wry>,
) -> Result<(), tauri::Error> {
let value = runtime_core
.frontend_debug_overlay_visible()
.map_err(|err| tauri::Error::AssetNotFound(err.to_string()))?;
app_handle.emit(EVENT_RUNTIME_DEBUG_OVERLAY_VISIBLE, value)
}
fn refresh_tray_menu_state(
runtime_core: &RuntimeCore,
tray_state: &TrayMenuState,
) -> Result<(), tauri::Error> {
let snapshot = runtime_core
.snapshot()
.read()
.map_err(|_| tauri::Error::AssetNotFound("snapshot lock poisoned".to_string()))?
.clone();
let debug_overlay_visible = runtime_core
.frontend_debug_overlay_visible()
.map_err(|err| tauri::Error::AssetNotFound(err.to_string()))?;
tray_state
.toggle_visibility
.set_text(visibility_menu_title(snapshot.flags.visible))
.map_err(|err| tauri::Error::AssetNotFound(err.to_string()))?;
tray_state
.toggle_always_on_top
.set_checked(snapshot.flags.always_on_top)
.map_err(|err| tauri::Error::AssetNotFound(err.to_string()))?;
tray_state
.toggle_debug_overlay
.set_checked(debug_overlay_visible)
.map_err(|err| tauri::Error::AssetNotFound(err.to_string()))?;
Ok(())
}
fn visibility_menu_title(visible: bool) -> &'static str {
if visible {
"Hide"
} else {
"Show"
}
}
fn state_name(value: FrontendState) -> &'static str {
match value {
FrontendState::Idle => "idle",