Add: tauri frontend as bevy alternative

This commit is contained in:
DaZuo0122
2026-02-13 09:57:08 +08:00
parent b0f462f63e
commit 3c3ca342c9
33 changed files with 11798 additions and 106 deletions

View File

@@ -0,0 +1,221 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use base64::Engine;
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 thiserror::Error;
use tokio::runtime::Runtime;
use tracing::warn;
const APP_NAME: &str = "sprimo";
const DEFAULT_PACK: &str = "default";
#[derive(Debug, Clone, serde::Serialize)]
struct UiAnimationClip {
name: String,
fps: u16,
frames: Vec<u32>,
one_shot: bool,
}
#[derive(Debug, Clone, serde::Serialize)]
struct UiAnchor {
x: f32,
y: f32,
}
#[derive(Debug, Clone, serde::Serialize)]
struct UiSpritePack {
id: String,
frame_width: u32,
frame_height: u32,
atlas_data_url: String,
animations: Vec<UiAnimationClip>,
anchor: UiAnchor,
}
#[derive(Debug, Clone, serde::Serialize)]
struct UiSnapshot {
state: String,
current_animation: String,
x: f32,
y: f32,
scale: f32,
active_sprite_pack: String,
}
#[derive(Debug, Error)]
enum AppError {
#[error("{0}")]
RuntimeCore(#[from] RuntimeCoreError),
#[error("tokio runtime init failed: {0}")]
Tokio(#[from] std::io::Error),
#[error("tauri runtime failed: {0}")]
Tauri(#[from] tauri::Error),
}
#[derive(Clone)]
struct AppState {
runtime_core: Arc<RuntimeCore>,
runtime: Arc<Runtime>,
}
#[tauri::command]
fn current_state(state: tauri::State<'_, AppState>) -> Result<UiSnapshot, String> {
let snapshot = state
.runtime_core
.snapshot()
.read()
.map_err(|_| "snapshot lock poisoned".to_string())?
.clone();
Ok(to_ui_snapshot(&snapshot))
}
#[tauri::command]
fn load_active_sprite_pack(state: tauri::State<'_, AppState>) -> Result<UiSpritePack, String> {
let config = state
.runtime_core
.config()
.read()
.map_err(|_| "config lock poisoned".to_string())?
.clone();
let root = std::env::current_dir()
.map_err(|err| err.to_string())?
.join("assets")
.join(config.sprite.sprite_packs_dir);
let selected = config.sprite.selected_pack;
let pack_path = match resolve_pack_path(&root, &selected) {
Ok(path) => path,
Err(_) => resolve_pack_path(&root, DEFAULT_PACK).map_err(|err| err.to_string())?,
};
let manifest = load_manifest(&pack_path).map_err(|err| err.to_string())?;
let image_path = pack_path.join(&manifest.image);
let image_bytes = std::fs::read(&image_path).map_err(|err| err.to_string())?;
let atlas_data_url = format!(
"data:image/png;base64,{}",
BASE64_STANDARD.encode(image_bytes)
);
Ok(UiSpritePack {
id: manifest.id,
frame_width: manifest.frame_width,
frame_height: manifest.frame_height,
atlas_data_url,
animations: manifest
.animations
.into_iter()
.map(to_ui_clip)
.collect(),
anchor: UiAnchor {
x: manifest.anchor.x,
y: manifest.anchor.y,
},
})
}
fn main() -> Result<(), AppError> {
tracing_subscriber::fmt()
.with_env_filter("sprimo=info")
.with_target(false)
.compact()
.init();
let platform: Arc<dyn PlatformAdapter> = create_adapter().into();
let runtime_core = Arc::new(RuntimeCore::new(APP_NAME, platform.capabilities())?);
let runtime = Arc::new(Runtime::new()?);
runtime_core.spawn_api(&runtime);
let state = AppState {
runtime_core: Arc::clone(&runtime_core),
runtime: Arc::clone(&runtime),
};
tauri::Builder::default()
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.manage(state)
.invoke_handler(tauri::generate_handler![current_state, load_active_sprite_pack])
.setup(|app| {
let app_state: tauri::State<'_, AppState> = app.state();
let runtime_core = Arc::clone(&app_state.runtime_core);
let runtime = Arc::clone(&app_state.runtime);
let app_handle = app.handle().clone();
if let Ok(snapshot) = runtime_core.snapshot().read() {
let _ = app_handle.emit("runtime:snapshot", to_ui_snapshot(&snapshot));
}
let command_rx = runtime_core.command_receiver();
runtime.spawn(async move {
loop {
let next = {
let mut receiver = command_rx.lock().await;
receiver.recv().await
};
let Some(envelope) = next else {
break;
};
if let Err(err) = runtime_core.apply_command(&envelope.command) {
warn!(%err, "failed to apply command in tauri runtime");
continue;
}
let payload = {
let snapshot = runtime_core.snapshot();
match snapshot.read() {
Ok(s) => Some(to_ui_snapshot(&s)),
Err(_) => None,
}
};
if let Some(value) = payload {
let _ = app_handle.emit("runtime:snapshot", value);
}
}
});
let _ = app;
Ok(())
})
.run(tauri::generate_context!())?;
Ok(())
}
fn to_ui_snapshot(snapshot: &FrontendStateSnapshot) -> UiSnapshot {
UiSnapshot {
state: state_name(snapshot.state).to_string(),
current_animation: snapshot.current_animation.clone(),
x: snapshot.x,
y: snapshot.y,
scale: snapshot.scale,
active_sprite_pack: snapshot.active_sprite_pack.clone(),
}
}
fn state_name(value: FrontendState) -> &'static str {
match value {
FrontendState::Idle => "idle",
FrontendState::Active => "active",
FrontendState::Success => "success",
FrontendState::Error => "error",
FrontendState::Dragging => "dragging",
FrontendState::Hidden => "hidden",
}
}
fn to_ui_clip(value: AnimationDefinition) -> UiAnimationClip {
UiAnimationClip {
name: value.name,
fps: value.fps.max(1),
frames: value.frames,
one_shot: value.one_shot.unwrap_or(false),
}
}