Add: tauri frontend as bevy alternative
This commit is contained in:
29
crates/sprimo-tauri/Cargo.toml
Normal file
29
crates/sprimo-tauri/Cargo.toml
Normal file
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "sprimo-tauri"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
build = "build.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.22.1"
|
||||
serde.workspace = true
|
||||
sprimo-config = { path = "../sprimo-config" }
|
||||
sprimo-platform = { path = "../sprimo-platform" }
|
||||
sprimo-sprite = { path = "../sprimo-sprite" }
|
||||
sprimo-runtime-core = { path = "../sprimo-runtime-core" }
|
||||
sprimo-protocol = { path = "../sprimo-protocol" }
|
||||
tauri = { version = "2.0.0", features = [] }
|
||||
tauri-plugin-global-shortcut = "2.0.0"
|
||||
tauri-plugin-log = "2.0.0"
|
||||
thiserror.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.0", features = [] }
|
||||
8
crates/sprimo-tauri/build.rs
Normal file
8
crates/sprimo-tauri/build.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=tauri.conf.json");
|
||||
println!("cargo:rerun-if-changed=src");
|
||||
println!("cargo:rerun-if-changed=../../frontend/tauri-ui/src");
|
||||
println!("cargo:rerun-if-changed=../../frontend/tauri-ui/index.html");
|
||||
println!("cargo:rerun-if-changed=../../frontend/tauri-ui/dist");
|
||||
tauri_build::build()
|
||||
}
|
||||
11
crates/sprimo-tauri/capabilities/default.json
Normal file
11
crates/sprimo-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Default capability for sprimo-tauri main window runtime APIs.",
|
||||
"windows": ["*"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:event:allow-listen",
|
||||
"core:event:allow-unlisten"
|
||||
]
|
||||
}
|
||||
1
crates/sprimo-tauri/gen/schemas/acl-manifests.json
Normal file
1
crates/sprimo-tauri/gen/schemas/acl-manifests.json
Normal file
File diff suppressed because one or more lines are too long
1
crates/sprimo-tauri/gen/schemas/capabilities.json
Normal file
1
crates/sprimo-tauri/gen/schemas/capabilities.json
Normal file
@@ -0,0 +1 @@
|
||||
{"default":{"identifier":"default","description":"Default capability for sprimo-tauri main window runtime APIs.","local":true,"windows":["*"],"permissions":["core:default","core:event:allow-listen","core:event:allow-unlisten"]}}
|
||||
2328
crates/sprimo-tauri/gen/schemas/desktop-schema.json
Normal file
2328
crates/sprimo-tauri/gen/schemas/desktop-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
2328
crates/sprimo-tauri/gen/schemas/windows-schema.json
Normal file
2328
crates/sprimo-tauri/gen/schemas/windows-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
crates/sprimo-tauri/icons/icon.ico
Normal file
BIN
crates/sprimo-tauri/icons/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 B |
221
crates/sprimo-tauri/src/main.rs
Normal file
221
crates/sprimo-tauri/src/main.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
30
crates/sprimo-tauri/tauri.conf.json
Normal file
30
crates/sprimo-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "sprimo-tauri",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.sprimo.tauri",
|
||||
"build": {
|
||||
"frontendDist": "../../frontend/tauri-ui/dist",
|
||||
"beforeDevCommand": "",
|
||||
"beforeBuildCommand": ""
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "sprimo-tauri",
|
||||
"width": 640,
|
||||
"height": 640,
|
||||
"decorations": false,
|
||||
"transparent": true,
|
||||
"alwaysOnTop": true,
|
||||
"resizable": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user