Add: tray for tauri
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user