Bug fix and UX improve #3
@@ -7,79 +7,79 @@
|
|||||||
"animations": [
|
"animations": [
|
||||||
{
|
{
|
||||||
"name": "idle",
|
"name": "idle",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [0, 1, 2, 3, 4, 5, 6, 7]
|
"frames": [0, 1, 2, 3, 4, 5, 6, 7]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "active",
|
"name": "active",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "happy",
|
"name": "happy",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "love",
|
"name": "love",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "excited",
|
"name": "excited",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [16, 17, 18, 19, 20, 21, 22, 23]
|
"frames": [16, 17, 18, 19, 20, 21, 22, 23]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "celebrate",
|
"name": "celebrate",
|
||||||
"fps": 10,
|
"fps": 2,
|
||||||
"frames": [16, 17, 18, 19, 20, 21, 22, 23],
|
"frames": [16, 17, 18, 19, 20, 21, 22, 23],
|
||||||
"one_shot": true
|
"one_shot": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "success",
|
"name": "success",
|
||||||
"fps": 10,
|
"fps": 2,
|
||||||
"frames": [16, 17, 18, 19, 20, 21, 22, 23],
|
"frames": [16, 17, 18, 19, 20, 21, 22, 23],
|
||||||
"one_shot": true
|
"one_shot": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "sleepy",
|
"name": "sleepy",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [24, 25, 26, 27, 28, 29, 30, 31]
|
"frames": [24, 25, 26, 27, 28, 29, 30, 31]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "snoring",
|
"name": "snoring",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [24, 25, 26, 27, 28, 29, 30, 31]
|
"frames": [24, 25, 26, 27, 28, 29, 30, 31]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "working",
|
"name": "working",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [32, 33, 34, 35, 36, 37, 38, 39]
|
"frames": [32, 33, 34, 35, 36, 37, 38, 39]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "angry",
|
"name": "angry",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "surprised",
|
"name": "surprised",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "shy",
|
"name": "shy",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "error",
|
"name": "error",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "dragging",
|
"name": "dragging",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [48, 49, 50, 51, 52, 53, 54, 55]
|
"frames": [48, 49, 50, 51, 52, 53, 54, 55]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -7,79 +7,79 @@
|
|||||||
"animations": [
|
"animations": [
|
||||||
{
|
{
|
||||||
"name": "idle",
|
"name": "idle",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [0, 1, 2, 3, 4, 5, 6, 7]
|
"frames": [0, 1, 2, 3, 4, 5, 6, 7]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "active",
|
"name": "active",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "happy",
|
"name": "happy",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "love",
|
"name": "love",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "excited",
|
"name": "excited",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [16, 17, 18, 19, 20, 21, 22, 23]
|
"frames": [16, 17, 18, 19, 20, 21, 22, 23]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "celebrate",
|
"name": "celebrate",
|
||||||
"fps": 10,
|
"fps": 2,
|
||||||
"frames": [16, 17, 18, 19, 20, 21, 22, 23],
|
"frames": [16, 17, 18, 19, 20, 21, 22, 23],
|
||||||
"one_shot": true
|
"one_shot": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "success",
|
"name": "success",
|
||||||
"fps": 10,
|
"fps": 2,
|
||||||
"frames": [16, 17, 18, 19, 20, 21, 22, 23],
|
"frames": [16, 17, 18, 19, 20, 21, 22, 23],
|
||||||
"one_shot": true
|
"one_shot": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "sleepy",
|
"name": "sleepy",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [24, 25, 26, 27, 28, 29, 30, 31]
|
"frames": [24, 25, 26, 27, 28, 29, 30, 31]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "snoring",
|
"name": "snoring",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [24, 25, 26, 27, 28, 29, 30, 31]
|
"frames": [24, 25, 26, 27, 28, 29, 30, 31]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "working",
|
"name": "working",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [32, 33, 34, 35, 36, 37, 38, 39]
|
"frames": [32, 33, 34, 35, 36, 37, 38, 39]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "angry",
|
"name": "angry",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "surprised",
|
"name": "surprised",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "shy",
|
"name": "shy",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "error",
|
"name": "error",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "dragging",
|
"name": "dragging",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [48, 49, 50, 51, 52, 53, 54, 55]
|
"frames": [48, 49, 50, 51, 52, 53, 54, 55]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -7,79 +7,79 @@
|
|||||||
"animations": [
|
"animations": [
|
||||||
{
|
{
|
||||||
"name": "idle",
|
"name": "idle",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [0, 1, 2, 3, 4, 5, 6, 7]
|
"frames": [0, 1, 2, 3, 4, 5, 6, 7]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "active",
|
"name": "active",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "happy",
|
"name": "happy",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "love",
|
"name": "love",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
"frames": [8, 9, 10, 11, 12, 13, 14, 15]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "excited",
|
"name": "excited",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [16, 17, 18, 19, 20, 21, 22, 23]
|
"frames": [16, 17, 18, 19, 20, 21, 22, 23]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "celebrate",
|
"name": "celebrate",
|
||||||
"fps": 10,
|
"fps": 2,
|
||||||
"frames": [16, 17, 18, 19, 20, 21, 22, 23],
|
"frames": [16, 17, 18, 19, 20, 21, 22, 23],
|
||||||
"one_shot": true
|
"one_shot": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "success",
|
"name": "success",
|
||||||
"fps": 10,
|
"fps": 2,
|
||||||
"frames": [16, 17, 18, 19, 20, 21, 22, 23],
|
"frames": [16, 17, 18, 19, 20, 21, 22, 23],
|
||||||
"one_shot": true
|
"one_shot": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "sleepy",
|
"name": "sleepy",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [24, 25, 26, 27, 28, 29, 30, 31]
|
"frames": [24, 25, 26, 27, 28, 29, 30, 31]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "snoring",
|
"name": "snoring",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [24, 25, 26, 27, 28, 29, 30, 31]
|
"frames": [24, 25, 26, 27, 28, 29, 30, 31]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "working",
|
"name": "working",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [32, 33, 34, 35, 36, 37, 38, 39]
|
"frames": [32, 33, 34, 35, 36, 37, 38, 39]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "angry",
|
"name": "angry",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "surprised",
|
"name": "surprised",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "shy",
|
"name": "shy",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "error",
|
"name": "error",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
"frames": [40, 41, 42, 43, 44, 45, 46, 47]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "dragging",
|
"name": "dragging",
|
||||||
"fps": 8,
|
"fps": 1,
|
||||||
"frames": [48, 49, 50, 51, 52, 53, 54, 55]
|
"frames": [48, 49, 50, 51, 52, 53, 54, 55]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ pub enum FrontendBackend {
|
|||||||
pub struct FrontendConfig {
|
pub struct FrontendConfig {
|
||||||
pub backend: FrontendBackend,
|
pub backend: FrontendBackend,
|
||||||
pub debug_overlay_visible: bool,
|
pub debug_overlay_visible: bool,
|
||||||
|
pub tauri_animation_slowdown_factor: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FrontendConfig {
|
impl Default for FrontendConfig {
|
||||||
@@ -166,6 +167,7 @@ impl Default for FrontendConfig {
|
|||||||
Self {
|
Self {
|
||||||
backend: FrontendBackend::Bevy,
|
backend: FrontendBackend::Bevy,
|
||||||
debug_overlay_visible: false,
|
debug_overlay_visible: false,
|
||||||
|
tauri_animation_slowdown_factor: 3,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,11 +227,13 @@ mod tests {
|
|||||||
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;
|
config.frontend.debug_overlay_visible = true;
|
||||||
|
config.frontend.tauri_animation_slowdown_factor = 7;
|
||||||
|
|
||||||
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);
|
assert!(loaded.frontend.debug_overlay_visible);
|
||||||
|
assert_eq!(loaded.frontend.tauri_animation_slowdown_factor, 7);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ use tokio::runtime::Runtime;
|
|||||||
use tokio::sync::{mpsc, Mutex};
|
use tokio::sync::{mpsc, Mutex};
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
|
const TAURI_ANIMATION_SLOWDOWN_FACTOR_MIN: u8 = 1;
|
||||||
|
const TAURI_ANIMATION_SLOWDOWN_FACTOR_MAX: u8 = 20;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum RuntimeCoreError {
|
pub enum RuntimeCoreError {
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
@@ -104,6 +107,32 @@ impl RuntimeCore {
|
|||||||
self.persist_config()
|
self.persist_config()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn frontend_tauri_animation_slowdown_factor(&self) -> Result<u8, RuntimeCoreError> {
|
||||||
|
let guard = self
|
||||||
|
.config
|
||||||
|
.read()
|
||||||
|
.map_err(|_| RuntimeCoreError::ConfigPoisoned)?;
|
||||||
|
Ok(clamp_tauri_animation_slowdown_factor(
|
||||||
|
guard.frontend.tauri_animation_slowdown_factor,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_frontend_tauri_animation_slowdown_factor(
|
||||||
|
&self,
|
||||||
|
value: u8,
|
||||||
|
) -> Result<u8, RuntimeCoreError> {
|
||||||
|
let clamped = clamp_tauri_animation_slowdown_factor(value);
|
||||||
|
{
|
||||||
|
let mut guard = self
|
||||||
|
.config
|
||||||
|
.write()
|
||||||
|
.map_err(|_| RuntimeCoreError::ConfigPoisoned)?;
|
||||||
|
guard.frontend.tauri_animation_slowdown_factor = clamped;
|
||||||
|
}
|
||||||
|
self.persist_config()?;
|
||||||
|
Ok(clamped)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn api_config(&self) -> ApiConfig {
|
pub fn api_config(&self) -> ApiConfig {
|
||||||
self.api_config.clone()
|
self.api_config.clone()
|
||||||
}
|
}
|
||||||
@@ -257,6 +286,13 @@ fn default_animation_for_state(state: sprimo_protocol::v1::FrontendState) -> &'s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clamp_tauri_animation_slowdown_factor(value: u8) -> u8 {
|
||||||
|
value.clamp(
|
||||||
|
TAURI_ANIMATION_SLOWDOWN_FACTOR_MIN,
|
||||||
|
TAURI_ANIMATION_SLOWDOWN_FACTOR_MAX,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn log_api_error(err: ApiServerError) {
|
fn log_api_error(err: ApiServerError) {
|
||||||
warn!(%err, "runtime core api server exited");
|
warn!(%err, "runtime core api server exited");
|
||||||
}
|
}
|
||||||
@@ -331,4 +367,36 @@ mod tests {
|
|||||||
core.set_frontend_debug_overlay_visible(true).expect("set");
|
core.set_frontend_debug_overlay_visible(true).expect("set");
|
||||||
assert!(core.frontend_debug_overlay_visible().expect("get"));
|
assert!(core.frontend_debug_overlay_visible().expect("get"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frontend_tauri_animation_slowdown_factor_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");
|
||||||
|
let persisted = core
|
||||||
|
.set_frontend_tauri_animation_slowdown_factor(6)
|
||||||
|
.expect("set");
|
||||||
|
assert_eq!(persisted, 6);
|
||||||
|
assert_eq!(
|
||||||
|
core.frontend_tauri_animation_slowdown_factor().expect("get"),
|
||||||
|
6
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frontend_tauri_animation_slowdown_factor_clamps() {
|
||||||
|
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");
|
||||||
|
let persisted = core
|
||||||
|
.set_frontend_tauri_animation_slowdown_factor(0)
|
||||||
|
.expect("set");
|
||||||
|
assert_eq!(persisted, 1);
|
||||||
|
assert_eq!(
|
||||||
|
core.frontend_tauri_animation_slowdown_factor().expect("get"),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const MENU_ID_TOGGLE_DEBUG_OVERLAY: &str = "toggle_debug_overlay";
|
|||||||
const MENU_ID_QUIT: &str = "quit";
|
const MENU_ID_QUIT: &str = "quit";
|
||||||
const EVENT_RUNTIME_SNAPSHOT: &str = "runtime:snapshot";
|
const EVENT_RUNTIME_SNAPSHOT: &str = "runtime:snapshot";
|
||||||
const EVENT_RUNTIME_DEBUG_OVERLAY_VISIBLE: &str = "runtime:debug-overlay-visible";
|
const EVENT_RUNTIME_DEBUG_OVERLAY_VISIBLE: &str = "runtime:debug-overlay-visible";
|
||||||
|
const EVENT_RUNTIME_ANIMATION_SLOWDOWN_FACTOR: &str = "runtime:animation-slowdown-factor";
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize)]
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
struct UiAnimationClip {
|
struct UiAnimationClip {
|
||||||
@@ -80,6 +81,7 @@ struct UiSettingsSnapshot {
|
|||||||
scale: f32,
|
scale: f32,
|
||||||
visible: bool,
|
visible: bool,
|
||||||
always_on_top: bool,
|
always_on_top: bool,
|
||||||
|
tauri_animation_slowdown_factor: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize)]
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
@@ -183,9 +185,37 @@ fn settings_snapshot(state: tauri::State<'_, AppState>) -> Result<UiSettingsSnap
|
|||||||
scale: snapshot.scale,
|
scale: snapshot.scale,
|
||||||
visible: snapshot.flags.visible,
|
visible: snapshot.flags.visible,
|
||||||
always_on_top: snapshot.flags.always_on_top,
|
always_on_top: snapshot.flags.always_on_top,
|
||||||
|
tauri_animation_slowdown_factor: state
|
||||||
|
.runtime_core
|
||||||
|
.frontend_tauri_animation_slowdown_factor()
|
||||||
|
.map_err(|err| err.to_string())?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn tauri_animation_slowdown_factor(state: tauri::State<'_, AppState>) -> Result<u8, String> {
|
||||||
|
state
|
||||||
|
.runtime_core
|
||||||
|
.frontend_tauri_animation_slowdown_factor()
|
||||||
|
.map_err(|err| err.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn set_tauri_animation_slowdown_factor(
|
||||||
|
app_handle: tauri::AppHandle,
|
||||||
|
state: tauri::State<'_, AppState>,
|
||||||
|
factor: u8,
|
||||||
|
) -> Result<u8, String> {
|
||||||
|
let persisted = state
|
||||||
|
.runtime_core
|
||||||
|
.set_frontend_tauri_animation_slowdown_factor(factor)
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
app_handle
|
||||||
|
.emit(EVENT_RUNTIME_ANIMATION_SLOWDOWN_FACTOR, persisted)
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
Ok(persisted)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn list_sprite_packs(state: tauri::State<'_, AppState>) -> Result<Vec<UiSpritePackOption>, String> {
|
fn list_sprite_packs(state: tauri::State<'_, AppState>) -> Result<Vec<UiSpritePackOption>, String> {
|
||||||
let root = sprite_pack_root(state.runtime_core.as_ref())?;
|
let root = sprite_pack_root(state.runtime_core.as_ref())?;
|
||||||
@@ -349,6 +379,8 @@ fn main() -> Result<(), AppError> {
|
|||||||
debug_overlay_visible,
|
debug_overlay_visible,
|
||||||
set_debug_overlay_visible,
|
set_debug_overlay_visible,
|
||||||
settings_snapshot,
|
settings_snapshot,
|
||||||
|
tauri_animation_slowdown_factor,
|
||||||
|
set_tauri_animation_slowdown_factor,
|
||||||
list_sprite_packs,
|
list_sprite_packs,
|
||||||
set_sprite_pack,
|
set_sprite_pack,
|
||||||
set_scale,
|
set_scale,
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ recovery_hotkey = "Ctrl+Alt+P"
|
|||||||
[frontend]
|
[frontend]
|
||||||
backend = "bevy"
|
backend = "bevy"
|
||||||
debug_overlay_visible = false
|
debug_overlay_visible = false
|
||||||
|
tauri_animation_slowdown_factor = 3
|
||||||
```
|
```
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
@@ -50,3 +51,6 @@ debug_overlay_visible = 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.
|
- `frontend.debug_overlay_visible` controls whether tauri window debug diagnostics panel is shown.
|
||||||
|
- `frontend.tauri_animation_slowdown_factor` controls tauri animation pacing multiplier.
|
||||||
|
valid range: `1..20`
|
||||||
|
effective frame interval: `(1000 / clip_fps) * factor`
|
||||||
|
|||||||
@@ -92,3 +92,8 @@ Default one-shot policy:
|
|||||||
|
|
||||||
- `celebrate` and `success` are one-shot.
|
- `celebrate` and `success` are one-shot.
|
||||||
- other row animations loop by default.
|
- other row animations loop by default.
|
||||||
|
|
||||||
|
Recommended FPS profile for 8-frame rows:
|
||||||
|
|
||||||
|
- looping rows: `1` fps
|
||||||
|
- one-shot `celebrate`/`success`: `2` fps
|
||||||
|
|||||||
@@ -115,7 +115,12 @@ An issue touching Tauri runtime behaviors must satisfy all requirements before `
|
|||||||
8. Chroma-key quality check:
|
8. Chroma-key quality check:
|
||||||
- verify no visible magenta background/fringe remains around sprite edges in normal runtime view,
|
- verify no visible magenta background/fringe remains around sprite edges in normal runtime view,
|
||||||
including non-exact gradient magenta atlas backgrounds (for example `ferris`/`demogorgon`)
|
including non-exact gradient magenta atlas backgrounds (for example `ferris`/`demogorgon`)
|
||||||
9. Scale anchor and bounds check:
|
9. Animation tempo check for 8-frame rows:
|
||||||
|
- looping row animations should feel very slow/readable (1 fps profile)
|
||||||
|
- one-shot `celebrate`/`success` should run slightly faster than loops (2 fps profile)
|
||||||
|
- tauri renderer applies an additional global slowdown factor (`x2`) over clip fps; verify perceived
|
||||||
|
playback matches this expectation
|
||||||
|
10. Scale anchor and bounds check:
|
||||||
- repeated scale changes should keep window centered without directional drift
|
- repeated scale changes should keep window centered without directional drift
|
||||||
- window must remain within current monitor bounds during scale adjustments
|
- window must remain within current monitor bounds during scale adjustments
|
||||||
- no runtime error is allowed from monitor lookup during scale operations (e.g. `currentMonitor`
|
- no runtime error is allowed from monitor lookup during scale operations (e.g. `currentMonitor`
|
||||||
@@ -138,10 +143,14 @@ An issue touching Tauri runtime behaviors must satisfy all requirements before `
|
|||||||
- main overlay auto-fits without clipping
|
- main overlay auto-fits without clipping
|
||||||
- persisted/runtime scale value reflects the effective fitted scale after monitor/window bounds
|
- persisted/runtime scale value reflects the effective fitted scale after monitor/window bounds
|
||||||
- value persists after restart
|
- value persists after restart
|
||||||
5. Toggle `Visible` and verify:
|
5. Change animation speed factor slider and verify:
|
||||||
|
- runtime animation pace updates immediately in main overlay
|
||||||
|
- value is clamped to `1..20`
|
||||||
|
- value persists after restart via `frontend.tauri_animation_slowdown_factor`
|
||||||
|
6. Toggle `Visible` and verify:
|
||||||
- main overlay hide/show behavior
|
- main overlay hide/show behavior
|
||||||
- persisted value survives restart
|
- persisted value survives restart
|
||||||
6. Toggle `Always on top` and verify:
|
7. Toggle `Always on top` and verify:
|
||||||
- main window z-order behavior updates
|
- main window z-order behavior updates
|
||||||
- persisted value survives restart
|
- persisted value survives restart
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type UiSettingsSnapshot = {
|
|||||||
scale: number;
|
scale: number;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
always_on_top: boolean;
|
always_on_top: boolean;
|
||||||
|
tauri_animation_slowdown_factor: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UiSpritePackOption = {
|
type UiSpritePackOption = {
|
||||||
@@ -33,6 +34,9 @@ const SCALE_MIN = 0.5;
|
|||||||
const SCALE_MAX = 3.0;
|
const SCALE_MAX = 3.0;
|
||||||
const LOGICAL_BASE_FRAME_WIDTH = 512;
|
const LOGICAL_BASE_FRAME_WIDTH = 512;
|
||||||
const LOGICAL_BASE_FRAME_HEIGHT = 512;
|
const LOGICAL_BASE_FRAME_HEIGHT = 512;
|
||||||
|
const SLOWDOWN_FACTOR_MIN = 1;
|
||||||
|
const SLOWDOWN_FACTOR_MAX = 20;
|
||||||
|
const SLOWDOWN_FACTOR_DEFAULT = 3;
|
||||||
|
|
||||||
async function invokeSetSpritePack(packIdOrPath: string): Promise<UiSnapshot> {
|
async function invokeSetSpritePack(packIdOrPath: string): Promise<UiSnapshot> {
|
||||||
return invoke<UiSnapshot>("set_sprite_pack", { packIdOrPath });
|
return invoke<UiSnapshot>("set_sprite_pack", { packIdOrPath });
|
||||||
@@ -50,6 +54,14 @@ async function invokeSetAlwaysOnTop(alwaysOnTop: boolean): Promise<UiSnapshot> {
|
|||||||
return invoke<UiSnapshot>("set_always_on_top", { alwaysOnTop });
|
return invoke<UiSnapshot>("set_always_on_top", { alwaysOnTop });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function invokeAnimationSlowdownFactor(): Promise<number> {
|
||||||
|
return invoke<number>("tauri_animation_slowdown_factor");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function invokeSetAnimationSlowdownFactor(factor: number): Promise<number> {
|
||||||
|
return invoke<number>("set_tauri_animation_slowdown_factor", { factor });
|
||||||
|
}
|
||||||
|
|
||||||
function fittedWindowSize(
|
function fittedWindowSize(
|
||||||
scale: number
|
scale: number
|
||||||
): { width: number; height: number } {
|
): { width: number; height: number } {
|
||||||
@@ -142,6 +154,7 @@ function MainOverlayWindow(): JSX.Element {
|
|||||||
const activePackRef = React.useRef<UiSpritePack | null>(null);
|
const activePackRef = React.useRef<UiSpritePack | null>(null);
|
||||||
const loadedPackKeyRef = React.useRef<string | null>(null);
|
const loadedPackKeyRef = React.useRef<string | null>(null);
|
||||||
const effectiveScaleSyncRef = React.useRef<number | null>(null);
|
const effectiveScaleSyncRef = React.useRef<number | null>(null);
|
||||||
|
const slowdownFactorRef = React.useRef<number>(SLOWDOWN_FACTOR_DEFAULT);
|
||||||
const loadingPackRef = React.useRef(false);
|
const loadingPackRef = React.useRef(false);
|
||||||
const mountedRef = React.useRef(false);
|
const mountedRef = React.useRef(false);
|
||||||
|
|
||||||
@@ -158,6 +171,7 @@ function MainOverlayWindow(): JSX.Element {
|
|||||||
}
|
}
|
||||||
const previousRenderer = rendererRef.current;
|
const previousRenderer = rendererRef.current;
|
||||||
const nextRenderer = await PixiPetRenderer.create(hostRef.current, pack, nextSnapshot);
|
const nextRenderer = await PixiPetRenderer.create(hostRef.current, pack, nextSnapshot);
|
||||||
|
nextRenderer.setAnimationSlowdownFactor(slowdownFactorRef.current);
|
||||||
rendererRef.current = nextRenderer;
|
rendererRef.current = nextRenderer;
|
||||||
activePackRef.current = pack;
|
activePackRef.current = pack;
|
||||||
loadedPackKeyRef.current = nextSnapshot.active_sprite_pack;
|
loadedPackKeyRef.current = nextSnapshot.active_sprite_pack;
|
||||||
@@ -252,13 +266,18 @@ function MainOverlayWindow(): JSX.Element {
|
|||||||
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")
|
invoke<boolean>("debug_overlay_visible"),
|
||||||
|
invokeAnimationSlowdownFactor()
|
||||||
])
|
])
|
||||||
.then(async ([pack, initialSnapshot, showDebug]) => {
|
.then(async ([pack, initialSnapshot, showDebug, slowdownFactor]) => {
|
||||||
if (!mountedRef.current) {
|
if (!mountedRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setDebugOverlayVisible(showDebug);
|
setDebugOverlayVisible(showDebug);
|
||||||
|
slowdownFactorRef.current = Math.min(
|
||||||
|
Math.max(Math.round(slowdownFactor), SLOWDOWN_FACTOR_MIN),
|
||||||
|
SLOWDOWN_FACTOR_MAX
|
||||||
|
);
|
||||||
await recreateRenderer(pack, initialSnapshot);
|
await recreateRenderer(pack, initialSnapshot);
|
||||||
await processSnapshot(initialSnapshot);
|
await processSnapshot(initialSnapshot);
|
||||||
|
|
||||||
@@ -271,9 +290,23 @@ function MainOverlayWindow(): JSX.Element {
|
|||||||
}
|
}
|
||||||
setDebugOverlayVisible(Boolean(event.payload));
|
setDebugOverlayVisible(Boolean(event.payload));
|
||||||
});
|
});
|
||||||
|
const unlistenSlowdown = await listen<number>(
|
||||||
|
"runtime:animation-slowdown-factor",
|
||||||
|
(event) => {
|
||||||
|
if (!mountedRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
slowdownFactorRef.current = Math.min(
|
||||||
|
Math.max(Math.round(Number(event.payload)), SLOWDOWN_FACTOR_MIN),
|
||||||
|
SLOWDOWN_FACTOR_MAX
|
||||||
|
);
|
||||||
|
rendererRef.current?.setAnimationSlowdownFactor(slowdownFactorRef.current);
|
||||||
|
}
|
||||||
|
);
|
||||||
unlisten = () => {
|
unlisten = () => {
|
||||||
unlistenSnapshot();
|
unlistenSnapshot();
|
||||||
unlistenDebug();
|
unlistenDebug();
|
||||||
|
unlistenSlowdown();
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -406,10 +439,29 @@ function SettingsWindow(): JSX.Element {
|
|||||||
active_sprite_pack: payload.active_sprite_pack,
|
active_sprite_pack: payload.active_sprite_pack,
|
||||||
scale: payload.scale,
|
scale: payload.scale,
|
||||||
visible: payload.visible,
|
visible: payload.visible,
|
||||||
always_on_top: payload.always_on_top
|
always_on_top: payload.always_on_top,
|
||||||
|
tauri_animation_slowdown_factor:
|
||||||
|
prev.tauri_animation_slowdown_factor ?? SLOWDOWN_FACTOR_DEFAULT
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
const unlistenSlowdown = await listen<number>("runtime:animation-slowdown-factor", (event) => {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const factor = Math.min(
|
||||||
|
Math.max(Math.round(Number(event.payload)), SLOWDOWN_FACTOR_MIN),
|
||||||
|
SLOWDOWN_FACTOR_MAX
|
||||||
|
);
|
||||||
|
setSettings((prev) =>
|
||||||
|
prev === null ? prev : { ...prev, tauri_animation_slowdown_factor: factor }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const previousUnlisten = unlisten;
|
||||||
|
unlisten = () => {
|
||||||
|
previousUnlisten();
|
||||||
|
unlistenSlowdown();
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -499,6 +551,29 @@ function SettingsWindow(): JSX.Element {
|
|||||||
[withPending]
|
[withPending]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onAnimationSlowdownFactorChange = React.useCallback(
|
||||||
|
async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = Number(event.target.value);
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const clamped = Math.min(
|
||||||
|
Math.max(Math.round(value), SLOWDOWN_FACTOR_MIN),
|
||||||
|
SLOWDOWN_FACTOR_MAX
|
||||||
|
);
|
||||||
|
const persisted = await withPending(() => invokeSetAnimationSlowdownFactor(clamped));
|
||||||
|
if (persisted === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSettings((prev) =>
|
||||||
|
prev === null
|
||||||
|
? prev
|
||||||
|
: { ...prev, tauri_animation_slowdown_factor: Number(persisted) }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[withPending]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="settings-root">
|
<main className="settings-root">
|
||||||
<section className="settings-card">
|
<section className="settings-card">
|
||||||
@@ -535,6 +610,20 @@ function SettingsWindow(): JSX.Element {
|
|||||||
onChange={onScaleChange}
|
onChange={onScaleChange}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<label className="field">
|
||||||
|
<span>
|
||||||
|
Animation Speed Factor: x{settings.tauri_animation_slowdown_factor}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={SLOWDOWN_FACTOR_MIN}
|
||||||
|
max={SLOWDOWN_FACTOR_MAX}
|
||||||
|
step={1}
|
||||||
|
value={settings.tauri_animation_slowdown_factor}
|
||||||
|
disabled={pending}
|
||||||
|
onChange={onAnimationSlowdownFactorChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
<label className="toggle">
|
<label className="toggle">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ const HALO_SAT_MIN = 0.15;
|
|||||||
const HALO_VAL_MIN = 0.04;
|
const HALO_VAL_MIN = 0.04;
|
||||||
const RENDER_FIT_PADDING = 16;
|
const RENDER_FIT_PADDING = 16;
|
||||||
const MIN_RENDER_SCALE = 0.01;
|
const MIN_RENDER_SCALE = 0.01;
|
||||||
|
const ANIMATION_SLOWDOWN_FACTOR_MIN = 1;
|
||||||
|
const ANIMATION_SLOWDOWN_FACTOR_MAX = 20;
|
||||||
|
const ANIMATION_SLOWDOWN_FACTOR_DEFAULT = 3;
|
||||||
|
|
||||||
export class PixiPetRenderer {
|
export class PixiPetRenderer {
|
||||||
private app: Application;
|
private app: Application;
|
||||||
@@ -63,6 +66,7 @@ export class PixiPetRenderer {
|
|||||||
private frameCursor = 0;
|
private frameCursor = 0;
|
||||||
private frameElapsedMs = 0;
|
private frameElapsedMs = 0;
|
||||||
private baseTexture: BaseTexture;
|
private baseTexture: BaseTexture;
|
||||||
|
private animationSlowdownFactor = ANIMATION_SLOWDOWN_FACTOR_DEFAULT;
|
||||||
private disposed = false;
|
private disposed = false;
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
@@ -431,13 +435,24 @@ export class PixiPetRenderer {
|
|||||||
this.layoutSprite();
|
this.layoutSprite();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setAnimationSlowdownFactor(factor: number): void {
|
||||||
|
if (!Number.isFinite(factor)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rounded = Math.round(factor);
|
||||||
|
this.animationSlowdownFactor = Math.min(
|
||||||
|
Math.max(rounded, ANIMATION_SLOWDOWN_FACTOR_MIN),
|
||||||
|
ANIMATION_SLOWDOWN_FACTOR_MAX
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private startTicker(): void {
|
private startTicker(): void {
|
||||||
this.app.ticker.add((ticker) => {
|
this.app.ticker.add((ticker) => {
|
||||||
if (this.disposed) {
|
if (this.disposed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.layoutSprite();
|
this.layoutSprite();
|
||||||
const frameMs = 1000 / Math.max(this.currentClip.fps, 1);
|
const frameMs = (1000 / Math.max(this.currentClip.fps, 1)) * this.animationSlowdownFactor;
|
||||||
this.frameElapsedMs += ticker.deltaMS;
|
this.frameElapsedMs += ticker.deltaMS;
|
||||||
if (this.frameElapsedMs < frameMs) {
|
if (this.frameElapsedMs < frameMs) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ P2
|
|||||||
|
|
||||||
Standardize `sprite.png` packs so each of the 7 rows maps to semantic animation groups, while
|
Standardize `sprite.png` packs so each of the 7 rows maps to semantic animation groups, while
|
||||||
keeping runtime compatibility with existing state names (`active`, `success`, `error`).
|
keeping runtime compatibility with existing state names (`active`, `success`, `error`).
|
||||||
|
Also retune 8-frame animation tempo to a slower profile for readability.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
@@ -42,6 +43,16 @@ One-shot defaults:
|
|||||||
2. All bundled sprite-pack manifests now expose row-based names and compatibility aliases.
|
2. All bundled sprite-pack manifests now expose row-based names and compatibility aliases.
|
||||||
3. Added runtime-core unit test to confirm `SetState::Dragging` selects `"dragging"`.
|
3. Added runtime-core unit test to confirm `SetState::Dragging` selects `"dragging"`.
|
||||||
4. Updated schema/design docs to formalize the row convention.
|
4. Updated schema/design docs to formalize the row convention.
|
||||||
|
5. Retuned bundled pack fps profile:
|
||||||
|
- loops: `1` fps
|
||||||
|
- one-shot `celebrate` and `success`: `2` fps
|
||||||
|
6. Tauri renderer-level pacing adjusted:
|
||||||
|
- added global frame interval slowdown factor (`x2`) in Pixi ticker to further reduce perceived speed
|
||||||
|
without changing manifest schema type (`fps` remains integer).
|
||||||
|
7. Added persisted tauri slowdown factor control:
|
||||||
|
- config key: `frontend.tauri_animation_slowdown_factor` (integer, range `1..20`, default `3`)
|
||||||
|
- new tauri commands/events to read/update factor at runtime
|
||||||
|
- settings window slider added for live tuning and persistence.
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user