Add: tauri frontend as bevy alternative
This commit is contained in:
@@ -26,6 +26,7 @@ pub struct AppConfig {
|
||||
pub api: ApiConfig,
|
||||
pub logging: LoggingConfig,
|
||||
pub controls: ControlsConfig,
|
||||
pub frontend: FrontendConfig,
|
||||
}
|
||||
|
||||
impl Default for AppConfig {
|
||||
@@ -37,6 +38,7 @@ impl Default for AppConfig {
|
||||
api: ApiConfig::default(),
|
||||
logging: LoggingConfig::default(),
|
||||
controls: ControlsConfig::default(),
|
||||
frontend: FrontendConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,6 +147,27 @@ impl Default for ControlsConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FrontendBackend {
|
||||
Bevy,
|
||||
Tauri,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct FrontendConfig {
|
||||
pub backend: FrontendBackend,
|
||||
}
|
||||
|
||||
impl Default for FrontendConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
backend: FrontendBackend::Bevy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn config_path(app_name: &str) -> Result<PathBuf, ConfigError> {
|
||||
let dirs =
|
||||
@@ -198,9 +221,11 @@ mod tests {
|
||||
let path = temp.path().join("config.toml");
|
||||
let mut config = AppConfig::default();
|
||||
config.window.x = 42.0;
|
||||
config.frontend.backend = super::FrontendBackend::Tauri;
|
||||
|
||||
save(&path, &config).expect("save");
|
||||
let (_, loaded) = load_or_create_at(&path).expect("reload");
|
||||
assert!((loaded.window.x - 42.0).abs() < f32::EPSILON);
|
||||
assert_eq!(loaded.frontend.backend, super::FrontendBackend::Tauri);
|
||||
}
|
||||
}
|
||||
|
||||
19
crates/sprimo-runtime-core/Cargo.toml
Normal file
19
crates/sprimo-runtime-core/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "sprimo-runtime-core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
sprimo-api = { path = "../sprimo-api" }
|
||||
sprimo-config = { path = "../sprimo-config" }
|
||||
sprimo-protocol = { path = "../sprimo-protocol" }
|
||||
thiserror.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.12.0"
|
||||
262
crates/sprimo-runtime-core/src/lib.rs
Normal file
262
crates/sprimo-runtime-core/src/lib.rs
Normal file
@@ -0,0 +1,262 @@
|
||||
use sprimo_api::{ApiConfig, ApiServerError, ApiState};
|
||||
use sprimo_config::{save, AppConfig, ConfigError};
|
||||
use sprimo_protocol::v1::{CapabilityFlags, CommandEnvelope, FrontendCommand, FrontendStateSnapshot};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use thiserror::Error;
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tracing::warn;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RuntimeCoreError {
|
||||
#[error("{0}")]
|
||||
Config(#[from] ConfigError),
|
||||
#[error("snapshot lock poisoned")]
|
||||
SnapshotPoisoned,
|
||||
#[error("config lock poisoned")]
|
||||
ConfigPoisoned,
|
||||
}
|
||||
|
||||
pub struct RuntimeCore {
|
||||
config_path: PathBuf,
|
||||
config: Arc<RwLock<AppConfig>>,
|
||||
snapshot: Arc<RwLock<FrontendStateSnapshot>>,
|
||||
api_config: ApiConfig,
|
||||
command_tx: mpsc::Sender<CommandEnvelope>,
|
||||
command_rx: Arc<Mutex<mpsc::Receiver<CommandEnvelope>>>,
|
||||
}
|
||||
|
||||
impl RuntimeCore {
|
||||
pub fn new(app_name: &str, capabilities: CapabilityFlags) -> Result<Self, RuntimeCoreError> {
|
||||
let (config_path, config_value) = sprimo_config::load_or_create(app_name)?;
|
||||
Self::new_with_config(config_path, config_value, capabilities)
|
||||
}
|
||||
|
||||
pub fn new_with_config(
|
||||
config_path: PathBuf,
|
||||
config_value: AppConfig,
|
||||
capabilities: CapabilityFlags,
|
||||
) -> Result<Self, RuntimeCoreError> {
|
||||
let mut snapshot = FrontendStateSnapshot::idle(capabilities);
|
||||
snapshot.x = config_value.window.x;
|
||||
snapshot.y = config_value.window.y;
|
||||
snapshot.scale = config_value.window.scale;
|
||||
snapshot.flags.click_through = config_value.window.click_through;
|
||||
snapshot.flags.always_on_top = config_value.window.always_on_top;
|
||||
snapshot.flags.visible = config_value.window.visible;
|
||||
snapshot.active_sprite_pack = config_value.sprite.selected_pack.clone();
|
||||
|
||||
let api_config = ApiConfig::default_with_token(config_value.api.auth_token.clone());
|
||||
let (command_tx, command_rx) = mpsc::channel(1_024);
|
||||
|
||||
Ok(Self {
|
||||
config_path,
|
||||
config: Arc::new(RwLock::new(config_value)),
|
||||
snapshot: Arc::new(RwLock::new(snapshot)),
|
||||
api_config,
|
||||
command_tx,
|
||||
command_rx: Arc::new(Mutex::new(command_rx)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> Arc<RwLock<FrontendStateSnapshot>> {
|
||||
Arc::clone(&self.snapshot)
|
||||
}
|
||||
|
||||
pub fn config(&self) -> Arc<RwLock<AppConfig>> {
|
||||
Arc::clone(&self.config)
|
||||
}
|
||||
|
||||
pub fn command_receiver(&self) -> Arc<Mutex<mpsc::Receiver<CommandEnvelope>>> {
|
||||
Arc::clone(&self.command_rx)
|
||||
}
|
||||
|
||||
pub fn command_sender(&self) -> mpsc::Sender<CommandEnvelope> {
|
||||
self.command_tx.clone()
|
||||
}
|
||||
|
||||
pub fn api_config(&self) -> ApiConfig {
|
||||
self.api_config.clone()
|
||||
}
|
||||
|
||||
pub fn spawn_api(&self, runtime: &Runtime) {
|
||||
let mut cfg = self.api_config.clone();
|
||||
if let Ok(guard) = self.config.read() {
|
||||
cfg.bind_addr = ([127, 0, 0, 1], guard.api.port).into();
|
||||
}
|
||||
let state = Arc::new(ApiState::new(
|
||||
cfg.clone(),
|
||||
Arc::clone(&self.snapshot),
|
||||
self.command_tx.clone(),
|
||||
));
|
||||
runtime.spawn(async move {
|
||||
if let Err(err) = sprimo_api::run_server(cfg, state).await {
|
||||
log_api_error(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn apply_command(&self, command: &FrontendCommand) -> Result<(), RuntimeCoreError> {
|
||||
match command {
|
||||
FrontendCommand::SetState { state, .. } => {
|
||||
let mut snapshot = self
|
||||
.snapshot
|
||||
.write()
|
||||
.map_err(|_| RuntimeCoreError::SnapshotPoisoned)?;
|
||||
snapshot.state = *state;
|
||||
snapshot.current_animation = default_animation_for_state(*state).to_string();
|
||||
snapshot.last_error = None;
|
||||
}
|
||||
FrontendCommand::PlayAnimation { name, .. } => {
|
||||
let mut snapshot = self
|
||||
.snapshot
|
||||
.write()
|
||||
.map_err(|_| RuntimeCoreError::SnapshotPoisoned)?;
|
||||
snapshot.current_animation = name.clone();
|
||||
snapshot.last_error = None;
|
||||
}
|
||||
FrontendCommand::SetSpritePack { pack_id_or_path } => {
|
||||
{
|
||||
let mut snapshot = self
|
||||
.snapshot
|
||||
.write()
|
||||
.map_err(|_| RuntimeCoreError::SnapshotPoisoned)?;
|
||||
snapshot.active_sprite_pack = pack_id_or_path.clone();
|
||||
snapshot.last_error = None;
|
||||
}
|
||||
{
|
||||
let mut config = self
|
||||
.config
|
||||
.write()
|
||||
.map_err(|_| RuntimeCoreError::ConfigPoisoned)?;
|
||||
config.sprite.selected_pack = pack_id_or_path.clone();
|
||||
}
|
||||
self.persist_config()?;
|
||||
}
|
||||
FrontendCommand::SetTransform { x, y, scale, .. } => {
|
||||
{
|
||||
let mut snapshot = self
|
||||
.snapshot
|
||||
.write()
|
||||
.map_err(|_| RuntimeCoreError::SnapshotPoisoned)?;
|
||||
if let Some(value) = x {
|
||||
snapshot.x = *value;
|
||||
}
|
||||
if let Some(value) = y {
|
||||
snapshot.y = *value;
|
||||
}
|
||||
if let Some(value) = scale {
|
||||
snapshot.scale = *value;
|
||||
}
|
||||
snapshot.last_error = None;
|
||||
}
|
||||
{
|
||||
let mut config = self
|
||||
.config
|
||||
.write()
|
||||
.map_err(|_| RuntimeCoreError::ConfigPoisoned)?;
|
||||
if let Some(value) = x {
|
||||
config.window.x = *value;
|
||||
}
|
||||
if let Some(value) = y {
|
||||
config.window.y = *value;
|
||||
}
|
||||
if let Some(value) = scale {
|
||||
config.window.scale = *value;
|
||||
}
|
||||
}
|
||||
self.persist_config()?;
|
||||
}
|
||||
FrontendCommand::SetFlags {
|
||||
click_through,
|
||||
always_on_top,
|
||||
visible,
|
||||
} => {
|
||||
{
|
||||
let mut snapshot = self
|
||||
.snapshot
|
||||
.write()
|
||||
.map_err(|_| RuntimeCoreError::SnapshotPoisoned)?;
|
||||
if let Some(value) = click_through {
|
||||
snapshot.flags.click_through = *value;
|
||||
}
|
||||
if let Some(value) = always_on_top {
|
||||
snapshot.flags.always_on_top = *value;
|
||||
}
|
||||
if let Some(value) = visible {
|
||||
snapshot.flags.visible = *value;
|
||||
}
|
||||
snapshot.last_error = None;
|
||||
}
|
||||
{
|
||||
let mut config = self
|
||||
.config
|
||||
.write()
|
||||
.map_err(|_| RuntimeCoreError::ConfigPoisoned)?;
|
||||
if let Some(value) = click_through {
|
||||
config.window.click_through = *value;
|
||||
}
|
||||
if let Some(value) = always_on_top {
|
||||
config.window.always_on_top = *value;
|
||||
}
|
||||
if let Some(value) = visible {
|
||||
config.window.visible = *value;
|
||||
}
|
||||
}
|
||||
self.persist_config()?;
|
||||
}
|
||||
FrontendCommand::Toast { .. } => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn persist_config(&self) -> Result<(), RuntimeCoreError> {
|
||||
let guard = self
|
||||
.config
|
||||
.read()
|
||||
.map_err(|_| RuntimeCoreError::ConfigPoisoned)?;
|
||||
save(&self.config_path, &guard)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn default_animation_for_state(state: sprimo_protocol::v1::FrontendState) -> &'static str {
|
||||
match state {
|
||||
sprimo_protocol::v1::FrontendState::Idle => "idle",
|
||||
sprimo_protocol::v1::FrontendState::Active => "active",
|
||||
sprimo_protocol::v1::FrontendState::Success => "success",
|
||||
sprimo_protocol::v1::FrontendState::Error => "error",
|
||||
sprimo_protocol::v1::FrontendState::Dragging => "idle",
|
||||
sprimo_protocol::v1::FrontendState::Hidden => "idle",
|
||||
}
|
||||
}
|
||||
|
||||
fn log_api_error(err: ApiServerError) {
|
||||
warn!(%err, "runtime core api server exited");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::RuntimeCore;
|
||||
use sprimo_config::AppConfig;
|
||||
use sprimo_protocol::v1::{CapabilityFlags, FrontendCommand, FrontendState};
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn state_command_updates_snapshot() {
|
||||
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.apply_command(&FrontendCommand::SetState {
|
||||
state: FrontendState::Active,
|
||||
ttl_ms: None,
|
||||
})
|
||||
.expect("apply");
|
||||
let snapshot = core.snapshot().read().expect("snapshot lock").clone();
|
||||
assert_eq!(snapshot.state, FrontendState::Active);
|
||||
assert_eq!(snapshot.current_animation, "active");
|
||||
}
|
||||
}
|
||||
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