fix(ui-dioxus): class-toggle animation instead of inline opacity (Blitz compatible)

This commit is contained in:
Hibryda 2026-03-19 07:11:01 +01:00
parent 67ab77ebf4
commit 2f03cf0ef0
4 changed files with 66 additions and 153 deletions

123
ui-dioxus/Cargo.lock generated
View file

@ -135,7 +135,6 @@ version = "0.1.0"
dependencies = [ dependencies = [
"agor-core", "agor-core",
"dioxus", "dioxus",
"dioxus-motion",
"log", "log",
"serde", "serde",
"serde_json", "serde_json",
@ -543,12 +542,6 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "base16"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d27c3610c36aee21ce8ac510e6224498de4228ad772a171ed65643a24693a5a8"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.22.1" version = "0.22.1"
@ -1355,7 +1348,6 @@ dependencies = [
"dioxus-html", "dioxus-html",
"dioxus-logger", "dioxus-logger",
"dioxus-native", "dioxus-native",
"dioxus-router",
"dioxus-signals", "dioxus-signals",
"dioxus-stores", "dioxus-stores",
"dioxus-web", "dioxus-web",
@ -1586,25 +1578,6 @@ dependencies = [
"tracing-wasm", "tracing-wasm",
] ]
[[package]]
name = "dioxus-motion"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddf087cf8951428be38394cff816a327f1d15f2953af8a65986578179dff21a6"
dependencies = [
"dioxus",
"easer",
"futures-channel",
"futures-util",
"instant",
"smallvec",
"spin_sleep",
"thiserror 2.0.18",
"tokio",
"tracing",
"wide 1.2.0",
]
[[package]] [[package]]
name = "dioxus-native" name = "dioxus-native"
version = "0.7.3" version = "0.7.3"
@ -1651,41 +1624,6 @@ dependencies = [
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
] ]
[[package]]
name = "dioxus-router"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d5b31f9e27231389bf5a117b7074d22d8c58358b484a2558e56fbab20e64ca4"
dependencies = [
"dioxus-cli-config",
"dioxus-core",
"dioxus-core-macro",
"dioxus-history",
"dioxus-hooks",
"dioxus-html",
"dioxus-router-macro",
"dioxus-signals",
"percent-encoding",
"rustversion",
"tracing",
"url",
]
[[package]]
name = "dioxus-router-macro"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "838b9b441a95da62b39cae4defd240b5ebb0ec9f2daea1126099e00a838dc86f"
dependencies = [
"base16",
"digest",
"proc-macro2",
"quote",
"sha2",
"slab",
"syn 2.0.117",
]
[[package]] [[package]]
name = "dioxus-rsx" name = "dioxus-rsx"
version = "0.7.3" version = "0.7.3"
@ -1873,15 +1811,6 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "easer"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fba524f8b83c9c5bde02c2bb1627de9d1f81980489a6d54168cdfd08c258f917"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "either" name = "either"
version = "1.15.0" version = "1.15.0"
@ -2979,15 +2908,6 @@ dependencies = [
"cfb", "cfb",
] ]
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "ioctl-rs" name = "ioctl-rs"
version = "0.1.6" version = "0.1.6"
@ -4868,15 +4788,6 @@ dependencies = [
"bytemuck", "bytemuck",
] ]
[[package]]
name = "safe_arch"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f7caad094bd561859bcd467734a720c3c1f5d1f338995351fefe2190c45efed"
dependencies = [
"bytemuck",
]
[[package]] [[package]]
name = "same-file" name = "same-file"
version = "1.0.6" version = "1.0.6"
@ -5119,17 +5030,6 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]] [[package]]
name = "sharded-slab" name = "sharded-slab"
version = "0.1.7" version = "0.1.7"
@ -5319,15 +5219,6 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "spin_sleep"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c07347b7c0301b9adba4350bdcf09c039d0e7160922050db0439b3c6723c8ab"
dependencies = [
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "spirv" name = "spirv"
version = "0.3.0+sdk-1.3.268.0" version = "0.3.0+sdk-1.3.268.0"
@ -6096,7 +5987,7 @@ version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a28554d13eb5daba527cc1b91b6c341372a0ae45ed277ffb2c6fbc04f319d7e" checksum = "6a28554d13eb5daba527cc1b91b6c341372a0ae45ed277ffb2c6fbc04f319d7e"
dependencies = [ dependencies = [
"wide 0.7.33", "wide",
] ]
[[package]] [[package]]
@ -6944,17 +6835,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"safe_arch 0.7.4", "safe_arch",
]
[[package]]
name = "wide"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "198f6abc41fab83526d10880fa5c17e2b4ee44e763949b4bb34e2fd1e8ca48e4"
dependencies = [
"bytemuck",
"safe_arch 1.0.0",
] ]
[[package]] [[package]]

View file

@ -15,5 +15,4 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
uuid = { version = "1", features = ["v4"] } uuid = { version = "1", features = ["v4"] }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
dioxus-motion = { version = "0.3", default-features = false, features = ["desktop"] }
log = "0.4" log = "0.4"

View file

@ -1,10 +1,15 @@
/// PulsingDot — Blitz-compatible animated status indicator using dioxus-motion. /// PulsingDot — Blitz-compatible animated status indicator.
/// ///
/// Uses dioxus-motion's Tween with LoopMode::Alternate for infinite ping-pong. /// Instead of animating opacity (Blitz doesn't reliably restyle inline style changes),
/// Avoids CSS @keyframes entirely — no Blitz full-scene repaint loop. /// we toggle between "bright" and "dim" CSS classes using a simple OS thread timer.
/// This changes the `class` attribute which Blitz DOES process for restyling.
///
/// CPU cost: schedule_update fires every 1s (2 updates per cycle), not 30fps.
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_motion::prelude::*; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub enum DotState { pub enum DotState {
@ -18,44 +23,43 @@ pub enum DotState {
#[component] #[component]
pub fn PulsingDot(state: DotState, size: Option<String>) -> Element { pub fn PulsingDot(state: DotState, size: Option<String>) -> Element {
let should_pulse = matches!(state, DotState::Running | DotState::Stalled); let should_pulse = matches!(state, DotState::Running | DotState::Stalled);
let mut opacity = use_motion(1.0f32); let is_dim = use_hook(|| Arc::new(AtomicBool::new(false)));
// Infinite ping-pong tween: 1.0 → 0.4 → 1.0 → ...
if should_pulse { if should_pulse {
use_effect(move || { let dim = is_dim.clone();
opacity.animate_to( let updater = use_hook(|| dioxus::dioxus_core::schedule_update());
0.4,
AnimationConfig::new(AnimationMode::Tween( use_hook(move || {
Tween::new(Duration::from_millis(1000)) std::thread::spawn(move || {
)) loop {
.with_loop(LoopMode::Alternate), std::thread::sleep(Duration::from_millis(1000));
); dim.fetch_xor(true, Ordering::Relaxed); // toggle
updater(); // trigger re-render
}
});
}); });
} }
let (color, shadow) = match state { let base_class = match state {
DotState::Running => ("var(--ctp-green)", "0 0 6px var(--ctp-green)"), DotState::Running => "dot-running",
DotState::Idle => ("var(--ctp-overlay0)", "none"), DotState::Idle => "dot-idle",
DotState::Stalled => ("var(--ctp-peach)", "0 0 4px var(--ctp-peach)"), DotState::Stalled => "dot-stalled",
DotState::Done => ("var(--ctp-blue)", "none"), DotState::Done => "dot-done",
DotState::Error => ("var(--ctp-red)", "0 0 4px var(--ctp-red)"), DotState::Error => "dot-error",
};
let dim_class = if should_pulse && is_dim.load(Ordering::Relaxed) {
"dot-dim"
} else {
""
}; };
let sz = size.unwrap_or_else(|| "8px".to_string()); let sz = size.unwrap_or_else(|| "8px".to_string());
let op = if should_pulse { opacity.get_value() } else { 1.0 };
rsx! { rsx! {
span { span {
style: " class: "pulsing-dot {base_class} {dim_class}",
display: inline-block; style: "width: {sz}; height: {sz};",
width: {sz};
height: {sz};
border-radius: 50%;
background: {color};
box-shadow: {shadow};
opacity: {op};
flex-shrink: 0;
",
} }
} }
} }

View file

@ -257,6 +257,35 @@ body {{
.status-dot.stalled {{ .status-dot.stalled {{
background: var(--ctp-peach); background: var(--ctp-peach);
}} }}
/* PulsingDot component classes */
.pulsing-dot {{
display: inline-block;
border-radius: 50%;
flex-shrink: 0;
}}
.dot-running {{
background: var(--ctp-green);
box-shadow: 0 0 6px var(--ctp-green);
}}
.dot-idle {{
background: var(--ctp-overlay0);
}}
.dot-stalled {{
background: var(--ctp-peach);
box-shadow: 0 0 4px var(--ctp-peach);
}}
.dot-done {{
background: var(--ctp-blue);
}}
.dot-error {{
background: var(--ctp-red);
box-shadow: 0 0 4px var(--ctp-red);
}}
.dot-dim {{
background: var(--ctp-surface1);
box-shadow: none;
}}
.status-dot.error {{ .status-dot.error {{
background: var(--ctp-red); background: var(--ctp-red);
}} }}