Compare commits
2 Commits
dev-bevy
...
907974e61f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
907974e61f | ||
|
|
e5e123cc84 |
@@ -158,12 +158,14 @@ pub enum FrontendBackend {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct FrontendConfig {
|
pub struct FrontendConfig {
|
||||||
pub backend: FrontendBackend,
|
pub backend: FrontendBackend,
|
||||||
|
pub debug_overlay_visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FrontendConfig {
|
impl Default for FrontendConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
backend: FrontendBackend::Bevy,
|
backend: FrontendBackend::Bevy,
|
||||||
|
debug_overlay_visible: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,10 +224,12 @@ mod tests {
|
|||||||
let mut config = AppConfig::default();
|
let mut config = AppConfig::default();
|
||||||
config.window.x = 42.0;
|
config.window.x = 42.0;
|
||||||
config.frontend.backend = super::FrontendBackend::Tauri;
|
config.frontend.backend = super::FrontendBackend::Tauri;
|
||||||
|
config.frontend.debug_overlay_visible = true;
|
||||||
|
|
||||||
save(&path, &config).expect("save");
|
save(&path, &config).expect("save");
|
||||||
let (_, loaded) = load_or_create_at(&path).expect("reload");
|
let (_, loaded) = load_or_create_at(&path).expect("reload");
|
||||||
assert!((loaded.window.x - 42.0).abs() < f32::EPSILON);
|
assert!((loaded.window.x - 42.0).abs() < f32::EPSILON);
|
||||||
assert_eq!(loaded.frontend.backend, super::FrontendBackend::Tauri);
|
assert_eq!(loaded.frontend.backend, super::FrontendBackend::Tauri);
|
||||||
|
assert!(loaded.frontend.debug_overlay_visible);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,28 @@ impl RuntimeCore {
|
|||||||
self.command_tx.clone()
|
self.command_tx.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn frontend_debug_overlay_visible(&self) -> Result<bool, RuntimeCoreError> {
|
||||||
|
let guard = self
|
||||||
|
.config
|
||||||
|
.read()
|
||||||
|
.map_err(|_| RuntimeCoreError::ConfigPoisoned)?;
|
||||||
|
Ok(guard.frontend.debug_overlay_visible)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_frontend_debug_overlay_visible(
|
||||||
|
&self,
|
||||||
|
visible: bool,
|
||||||
|
) -> Result<(), RuntimeCoreError> {
|
||||||
|
{
|
||||||
|
let mut guard = self
|
||||||
|
.config
|
||||||
|
.write()
|
||||||
|
.map_err(|_| RuntimeCoreError::ConfigPoisoned)?;
|
||||||
|
guard.frontend.debug_overlay_visible = visible;
|
||||||
|
}
|
||||||
|
self.persist_config()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn api_config(&self) -> ApiConfig {
|
pub fn api_config(&self) -> ApiConfig {
|
||||||
self.api_config.clone()
|
self.api_config.clone()
|
||||||
}
|
}
|
||||||
@@ -283,4 +305,14 @@ mod tests {
|
|||||||
let config = core.config().read().expect("config lock").clone();
|
let config = core.config().read().expect("config lock").clone();
|
||||||
assert!(!config.window.click_through);
|
assert!(!config.window.click_through);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frontend_debug_overlay_visibility_roundtrips() {
|
||||||
|
let temp = TempDir::new().expect("tempdir");
|
||||||
|
let path = temp.path().join("config.toml");
|
||||||
|
let core = RuntimeCore::new_with_config(path, AppConfig::default(), CapabilityFlags::default())
|
||||||
|
.expect("core init");
|
||||||
|
core.set_frontend_debug_overlay_visible(true).expect("set");
|
||||||
|
assert!(core.frontend_debug_overlay_visible().expect("get"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ sprimo-platform = { path = "../sprimo-platform" }
|
|||||||
sprimo-sprite = { path = "../sprimo-sprite" }
|
sprimo-sprite = { path = "../sprimo-sprite" }
|
||||||
sprimo-runtime-core = { path = "../sprimo-runtime-core" }
|
sprimo-runtime-core = { path = "../sprimo-runtime-core" }
|
||||||
sprimo-protocol = { path = "../sprimo-protocol" }
|
sprimo-protocol = { path = "../sprimo-protocol" }
|
||||||
tauri = { version = "2.0.0", features = [] }
|
tauri = { version = "2.0.0", features = ["tray-icon"] }
|
||||||
tauri-plugin-global-shortcut = "2.0.0"
|
tauri-plugin-global-shortcut = "2.0.0"
|
||||||
tauri-plugin-log = "2.0.0"
|
tauri-plugin-log = "2.0.0"
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
|
|||||||
@@ -2,18 +2,29 @@
|
|||||||
|
|
||||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
|
use sprimo_protocol::v1::FrontendCommand;
|
||||||
use sprimo_platform::{create_adapter, PlatformAdapter};
|
use sprimo_platform::{create_adapter, PlatformAdapter};
|
||||||
use sprimo_protocol::v1::{FrontendState, FrontendStateSnapshot};
|
use sprimo_protocol::v1::{FrontendState, FrontendStateSnapshot};
|
||||||
use sprimo_runtime_core::{RuntimeCore, RuntimeCoreError};
|
use sprimo_runtime_core::{RuntimeCore, RuntimeCoreError};
|
||||||
use sprimo_sprite::{load_manifest, resolve_pack_path, AnimationDefinition};
|
use sprimo_sprite::{load_manifest, resolve_pack_path, AnimationDefinition};
|
||||||
use std::sync::Arc;
|
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 thiserror::Error;
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
const APP_NAME: &str = "sprimo";
|
const APP_NAME: &str = "sprimo";
|
||||||
const DEFAULT_PACK: &str = "default";
|
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)]
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
struct UiAnimationClip {
|
struct UiAnimationClip {
|
||||||
@@ -65,6 +76,13 @@ struct AppState {
|
|||||||
runtime: Arc<Runtime>,
|
runtime: Arc<Runtime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct TrayMenuState {
|
||||||
|
toggle_visibility: MenuItem<Wry>,
|
||||||
|
toggle_always_on_top: CheckMenuItem<Wry>,
|
||||||
|
toggle_debug_overlay: CheckMenuItem<Wry>,
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn current_state(state: tauri::State<'_, AppState>) -> Result<UiSnapshot, String> {
|
fn current_state(state: tauri::State<'_, AppState>) -> Result<UiSnapshot, String> {
|
||||||
let snapshot = state
|
let snapshot = state
|
||||||
@@ -121,6 +139,28 @@ fn load_active_sprite_pack(state: tauri::State<'_, AppState>) -> Result<UiSprite
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn debug_overlay_visible(state: tauri::State<'_, AppState>) -> Result<bool, String> {
|
||||||
|
state
|
||||||
|
.runtime_core
|
||||||
|
.frontend_debug_overlay_visible()
|
||||||
|
.map_err(|err| err.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn set_debug_overlay_visible(
|
||||||
|
app_handle: tauri::AppHandle,
|
||||||
|
state: tauri::State<'_, AppState>,
|
||||||
|
visible: bool,
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
state
|
||||||
|
.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)
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), AppError> {
|
fn main() -> Result<(), AppError> {
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_env_filter("sprimo=info")
|
.with_env_filter("sprimo=info")
|
||||||
@@ -141,20 +181,29 @@ fn main() -> Result<(), AppError> {
|
|||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||||
.manage(state)
|
.manage(state)
|
||||||
.invoke_handler(tauri::generate_handler![current_state, load_active_sprite_pack])
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
current_state,
|
||||||
|
load_active_sprite_pack,
|
||||||
|
debug_overlay_visible,
|
||||||
|
set_debug_overlay_visible
|
||||||
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let app_state: tauri::State<'_, AppState> = app.state();
|
let app_state: tauri::State<'_, AppState> = app.state();
|
||||||
let runtime_core = Arc::clone(&app_state.runtime_core);
|
let runtime_core = Arc::clone(&app_state.runtime_core);
|
||||||
let runtime = Arc::clone(&app_state.runtime);
|
let runtime = Arc::clone(&app_state.runtime);
|
||||||
let app_handle = app.handle().clone();
|
let app_handle = app.handle().clone();
|
||||||
|
|
||||||
|
let tray_state = setup_tray(&app_handle, &runtime_core)?;
|
||||||
if let Ok(snapshot) = runtime_core.snapshot().read() {
|
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 command_rx = runtime_core.command_receiver();
|
||||||
let runtime_core_for_commands = Arc::clone(&runtime_core);
|
let runtime_core_for_commands = Arc::clone(&runtime_core);
|
||||||
let app_handle_for_commands = app_handle.clone();
|
let app_handle_for_commands = app_handle.clone();
|
||||||
|
let tray_state_for_commands = tray_state.clone();
|
||||||
runtime.spawn(async move {
|
runtime.spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
let next = {
|
let next = {
|
||||||
@@ -178,7 +227,11 @@ fn main() -> Result<(), AppError> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Some(value) = payload {
|
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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -186,6 +239,7 @@ fn main() -> Result<(), AppError> {
|
|||||||
if let Some(window) = app.get_webview_window("main") {
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
let runtime_core = Arc::clone(&runtime_core);
|
let runtime_core = Arc::clone(&runtime_core);
|
||||||
let app_handle = app_handle.clone();
|
let app_handle = app_handle.clone();
|
||||||
|
let tray_state_for_window = tray_state.clone();
|
||||||
window.on_window_event(move |event| {
|
window.on_window_event(move |event| {
|
||||||
if let tauri::WindowEvent::Moved(position) = event {
|
if let tauri::WindowEvent::Moved(position) = event {
|
||||||
let command = sprimo_protocol::v1::FrontendCommand::SetTransform {
|
let command = sprimo_protocol::v1::FrontendCommand::SetTransform {
|
||||||
@@ -197,13 +251,34 @@ fn main() -> Result<(), AppError> {
|
|||||||
};
|
};
|
||||||
if runtime_core.apply_command(&command).is_ok() {
|
if runtime_core.apply_command(&command).is_ok() {
|
||||||
if let Ok(snapshot) = runtime_core.snapshot().read() {
|
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;
|
let _ = app;
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -223,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 {
|
fn state_name(value: FrontendState) -> &'static str {
|
||||||
match value {
|
match value {
|
||||||
FrontendState::Idle => "idle",
|
FrontendState::Idle => "idle",
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ recovery_hotkey = "Ctrl+Alt+P"
|
|||||||
|
|
||||||
[frontend]
|
[frontend]
|
||||||
backend = "bevy"
|
backend = "bevy"
|
||||||
|
debug_overlay_visible = false
|
||||||
```
|
```
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
@@ -48,3 +49,4 @@ backend = "bevy"
|
|||||||
- `window.click_through` is deprecated and ignored at runtime; it is always forced to `false`.
|
- `window.click_through` is deprecated and ignored at runtime; it is always forced to `false`.
|
||||||
- On Windows, `recovery_hotkey` now forces `visible = true` and `always_on_top = true` for recovery.
|
- On Windows, `recovery_hotkey` now forces `visible = true` and `always_on_top = true` for recovery.
|
||||||
- `frontend.backend` selects runtime frontend implementation (`bevy` or `tauri`).
|
- `frontend.backend` selects runtime frontend implementation (`bevy` or `tauri`).
|
||||||
|
- `frontend.debug_overlay_visible` controls whether tauri window debug diagnostics panel is shown.
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Date: 2026-02-12
|
|||||||
| Random backend API tester | Implemented | `scripts/random_backend_tester.py` with `just random-backend-test` and strict variant |
|
| Random backend API tester | Implemented | `scripts/random_backend_tester.py` with `just random-backend-test` and strict variant |
|
||||||
| QA/documentation workflow | Implemented | `docs/QA_WORKFLOW.md`, issue/evidence templates, and `scripts/qa_validate.py` with `just qa-validate` |
|
| QA/documentation workflow | Implemented | `docs/QA_WORKFLOW.md`, issue/evidence templates, and `scripts/qa_validate.py` with `just qa-validate` |
|
||||||
| Shared runtime core | Implemented | `sprimo-runtime-core` now backs both Tauri and Bevy startup, snapshot/config ownership, and API wiring |
|
| Shared runtime core | Implemented | `sprimo-runtime-core` now backs both Tauri and Bevy startup, snapshot/config ownership, and API wiring |
|
||||||
| Tauri alternative frontend | In progress | `sprimo-tauri` now runs runtime-core/API + PixiJS sprite rendering shell; scale updates now auto-fit window to avoid top clipping |
|
| Tauri alternative frontend | In progress | `sprimo-tauri` now runs runtime-core/API + PixiJS sprite rendering shell; scale auto-fit, persisted debug-overlay toggle, and Windows-first tray/menu MVP are implemented |
|
||||||
| Tauri runtime testing workflow | Implemented | `docs/TAURI_RUNTIME_TESTING.md` defines strict workspace testing; packaged mode pending packaging support |
|
| Tauri runtime testing workflow | Implemented | `docs/TAURI_RUNTIME_TESTING.md` defines strict workspace testing; packaged mode pending packaging support |
|
||||||
|
|
||||||
## Next Major Gaps
|
## Next Major Gaps
|
||||||
@@ -28,4 +28,4 @@ Date: 2026-02-12
|
|||||||
2. Linux/macOS overlay behavior remains best-effort with no-op platform adapter.
|
2. Linux/macOS overlay behavior remains best-effort with no-op platform adapter.
|
||||||
3. `/v1/state` diagnostics are minimal; error history is not persisted beyond latest runtime error.
|
3. `/v1/state` diagnostics are minimal; error history is not persisted beyond latest runtime error.
|
||||||
4. Issue 1 runtime after-fix screenshot evidence is still pending before closure.
|
4. Issue 1 runtime after-fix screenshot evidence is still pending before closure.
|
||||||
5. `sprimo-tauri` still lacks tray/menu/window-behavior parity and full acceptance parity tests.
|
5. `sprimo-tauri` still lacks cross-platform tray/menu parity and full acceptance parity tests.
|
||||||
|
|||||||
@@ -47,16 +47,22 @@ Frontend:
|
|||||||
- Tauri backend exposes:
|
- Tauri backend exposes:
|
||||||
- `current_state` command (structured snapshot DTO)
|
- `current_state` command (structured snapshot DTO)
|
||||||
- `load_active_sprite_pack` command (manifest + atlas as base64 data URL)
|
- `load_active_sprite_pack` command (manifest + atlas as base64 data URL)
|
||||||
|
- `debug_overlay_visible` / `set_debug_overlay_visible` commands for persisted debug panel control
|
||||||
- `runtime:snapshot` event after command application.
|
- `runtime:snapshot` event after command application.
|
||||||
- React/Vite frontend now renders sprite atlas frames with PixiJS and updates animation/scale
|
- React/Vite frontend now renders sprite atlas frames with PixiJS and updates animation/scale
|
||||||
from runtime snapshot events.
|
from runtime snapshot events.
|
||||||
- Tauri window drag is implemented for undecorated mode:
|
- Tauri window drag is implemented for undecorated mode:
|
||||||
- left-mouse drag starts native window dragging
|
- left-mouse drag starts native window dragging
|
||||||
- moved position is synced into runtime-core snapshot/config state.
|
- moved position is synced into runtime-core snapshot/config state.
|
||||||
|
- Windows-first tray/menu MVP is implemented:
|
||||||
|
- `Show/Hide`
|
||||||
|
- `Always on top` toggle
|
||||||
|
- `Debug overlay` toggle
|
||||||
|
- `Quit`
|
||||||
- Bevy frontend remains intact.
|
- Bevy frontend remains intact.
|
||||||
- Runtime QA workflow is defined in `docs/TAURI_RUNTIME_TESTING.md`.
|
- Runtime QA workflow is defined in `docs/TAURI_RUNTIME_TESTING.md`.
|
||||||
|
|
||||||
## Remaining Work
|
## Remaining Work
|
||||||
|
|
||||||
1. Add tray/menu parity and window behavior parity with Bevy path.
|
1. Extend tray/menu implementation beyond Windows-first MVP and close platform parity gaps.
|
||||||
2. Add frontend parity acceptance tests (Bevy vs Tauri state transitions).
|
2. Add frontend parity acceptance tests (Bevy vs Tauri state transitions).
|
||||||
|
|||||||
@@ -74,6 +74,16 @@ An issue touching Tauri runtime behaviors must satisfy all requirements before `
|
|||||||
- left-mouse drag moves the window
|
- left-mouse drag moves the window
|
||||||
- window remains non-resizable
|
- window remains non-resizable
|
||||||
- moved position is reflected in runtime snapshot state (`x`, `y`) and persists after restart
|
- moved position is reflected in runtime snapshot state (`x`, `y`) and persists after restart
|
||||||
|
9. Verify debug-overlay visibility control:
|
||||||
|
- default startup behavior follows `frontend.debug_overlay_visible` config
|
||||||
|
- `debug_overlay_visible`/`set_debug_overlay_visible` invoke commands toggle panel at runtime
|
||||||
|
- toggle state persists after restart
|
||||||
|
10. Verify Windows tray/menu controls:
|
||||||
|
- tray left-click opens menu without directly toggling visibility
|
||||||
|
- `Show/Hide` toggles window visibility and persists state
|
||||||
|
- `Always on top` toggles top-most behavior and persists state
|
||||||
|
- `Debug overlay` toggles panel visibility and persists state
|
||||||
|
- `Quit` exits cleanly and preserves current persisted visibility/top-most/debug settings
|
||||||
|
|
||||||
## API + Runtime Contract Checklist
|
## API + Runtime Contract Checklist
|
||||||
|
|
||||||
@@ -91,6 +101,7 @@ An issue touching Tauri runtime behaviors must satisfy all requirements before `
|
|||||||
- `current_state` output parsed successfully.
|
- `current_state` output parsed successfully.
|
||||||
- `load_active_sprite_pack` returns expected fields.
|
- `load_active_sprite_pack` returns expected fields.
|
||||||
- `runtime:snapshot` event received on runtime command changes.
|
- `runtime:snapshot` event received on runtime command changes.
|
||||||
|
- `debug_overlay_visible` and `set_debug_overlay_visible` invoke commands work and persist config.
|
||||||
|
|
||||||
## Evidence Requirements
|
## Evidence Requirements
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ async function fitWindowForScale(pack: UiSpritePack, scale: number): Promise<voi
|
|||||||
function App(): JSX.Element {
|
function App(): JSX.Element {
|
||||||
const [snapshot, setSnapshot] = React.useState<UiSnapshot | null>(null);
|
const [snapshot, setSnapshot] = React.useState<UiSnapshot | null>(null);
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
const [debugOverlayVisible, setDebugOverlayVisible] = React.useState(false);
|
||||||
const hostRef = React.useRef<HTMLDivElement | null>(null);
|
const hostRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
const rendererRef = React.useRef<PixiPetRenderer | null>(null);
|
const rendererRef = React.useRef<PixiPetRenderer | null>(null);
|
||||||
const scaleFitRef = React.useRef<number | null>(null);
|
const scaleFitRef = React.useRef<number | null>(null);
|
||||||
@@ -58,13 +59,15 @@ function App(): JSX.Element {
|
|||||||
let activePack: UiSpritePack | null = null;
|
let activePack: UiSpritePack | null = null;
|
||||||
Promise.all([
|
Promise.all([
|
||||||
invoke<UiSpritePack>("load_active_sprite_pack"),
|
invoke<UiSpritePack>("load_active_sprite_pack"),
|
||||||
invoke<UiSnapshot>("current_state")
|
invoke<UiSnapshot>("current_state"),
|
||||||
|
invoke<boolean>("debug_overlay_visible")
|
||||||
])
|
])
|
||||||
.then(async ([pack, initialSnapshot]) => {
|
.then(async ([pack, initialSnapshot, showDebug]) => {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
activePack = pack;
|
activePack = pack;
|
||||||
|
setDebugOverlayVisible(showDebug);
|
||||||
setSnapshot(initialSnapshot);
|
setSnapshot(initialSnapshot);
|
||||||
if (hostRef.current !== null) {
|
if (hostRef.current !== null) {
|
||||||
rendererRef.current = await PixiPetRenderer.create(
|
rendererRef.current = await PixiPetRenderer.create(
|
||||||
@@ -99,6 +102,20 @@ function App(): JSX.Element {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
const unlistenDebug = await listen<boolean>(
|
||||||
|
"runtime:debug-overlay-visible",
|
||||||
|
(event) => {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setDebugOverlayVisible(Boolean(event.payload));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const previousUnlisten = unlisten;
|
||||||
|
unlisten = () => {
|
||||||
|
previousUnlisten();
|
||||||
|
unlistenDebug();
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -115,6 +132,32 @@ function App(): JSX.Element {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const toggleDebugOverlay = React.useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const next = !debugOverlayVisible;
|
||||||
|
const persisted = await invoke<boolean>("set_debug_overlay_visible", {
|
||||||
|
visible: next
|
||||||
|
});
|
||||||
|
setDebugOverlayVisible(persisted);
|
||||||
|
} catch (err) {
|
||||||
|
setError(String(err));
|
||||||
|
}
|
||||||
|
}, [debugOverlayVisible]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const onKeyDown = (event: KeyboardEvent): void => {
|
||||||
|
if (!event.ctrlKey || !event.shiftKey || event.code !== "KeyD") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
void toggleDebugOverlay();
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", onKeyDown);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", onKeyDown);
|
||||||
|
};
|
||||||
|
}, [toggleDebugOverlay]);
|
||||||
|
|
||||||
const onMouseDown = React.useCallback((event: React.MouseEvent<HTMLElement>) => {
|
const onMouseDown = React.useCallback((event: React.MouseEvent<HTMLElement>) => {
|
||||||
if (event.button !== 0) {
|
if (event.button !== 0) {
|
||||||
return;
|
return;
|
||||||
@@ -127,28 +170,32 @@ function App(): JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<main className="app" onMouseDown={onMouseDown}>
|
<main className="app" onMouseDown={onMouseDown}>
|
||||||
<div className="canvas-host" ref={hostRef} />
|
<div className="canvas-host" ref={hostRef} />
|
||||||
<section className="debug-panel">
|
{error !== null && !debugOverlayVisible ? <p className="error-banner">{error}</p> : null}
|
||||||
<h1>sprimo-tauri</h1>
|
{debugOverlayVisible ? (
|
||||||
{error !== null ? <p className="error">{error}</p> : null}
|
<section className="debug-panel">
|
||||||
{snapshot === null ? (
|
<h1>sprimo-tauri</h1>
|
||||||
<p>Loading snapshot...</p>
|
<p className="hint">Toggle: Ctrl+Shift+D</p>
|
||||||
) : (
|
{error !== null ? <p className="error">{error}</p> : null}
|
||||||
<dl>
|
{snapshot === null ? (
|
||||||
<dt>state</dt>
|
<p>Loading snapshot...</p>
|
||||||
<dd>{snapshot.state}</dd>
|
) : (
|
||||||
<dt>animation</dt>
|
<dl>
|
||||||
<dd>{snapshot.current_animation}</dd>
|
<dt>state</dt>
|
||||||
<dt>pack</dt>
|
<dd>{snapshot.state}</dd>
|
||||||
<dd>{snapshot.active_sprite_pack}</dd>
|
<dt>animation</dt>
|
||||||
<dt>position</dt>
|
<dd>{snapshot.current_animation}</dd>
|
||||||
<dd>
|
<dt>pack</dt>
|
||||||
{snapshot.x}, {snapshot.y}
|
<dd>{snapshot.active_sprite_pack}</dd>
|
||||||
</dd>
|
<dt>position</dt>
|
||||||
<dt>scale</dt>
|
<dd>
|
||||||
<dd>{snapshot.scale}</dd>
|
{snapshot.x}, {snapshot.y}
|
||||||
</dl>
|
</dd>
|
||||||
)}
|
<dt>scale</dt>
|
||||||
</section>
|
<dd>{snapshot.scale}</dd>
|
||||||
|
</dl>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,3 +53,23 @@ dd {
|
|||||||
.error {
|
.error {
|
||||||
color: #fecaca;
|
color: #fecaca;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-banner {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
left: 8px;
|
||||||
|
max-width: 320px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
background: rgba(127, 29, 29, 0.75);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fee2e2;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user