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 = [
"agor-core",
"dioxus",
"dioxus-motion",
"log",
"serde",
"serde_json",
@ -543,12 +542,6 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "base16"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d27c3610c36aee21ce8ac510e6224498de4228ad772a171ed65643a24693a5a8"
[[package]]
name = "base64"
version = "0.22.1"
@ -1355,7 +1348,6 @@ dependencies = [
"dioxus-html",
"dioxus-logger",
"dioxus-native",
"dioxus-router",
"dioxus-signals",
"dioxus-stores",
"dioxus-web",
@ -1586,25 +1578,6 @@ dependencies = [
"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]]
name = "dioxus-native"
version = "0.7.3"
@ -1651,41 +1624,6 @@ dependencies = [
"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]]
name = "dioxus-rsx"
version = "0.7.3"
@ -1873,15 +1811,6 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "either"
version = "1.15.0"
@ -2979,15 +2908,6 @@ dependencies = [
"cfb",
]
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
]
[[package]]
name = "ioctl-rs"
version = "0.1.6"
@ -4868,15 +4788,6 @@ dependencies = [
"bytemuck",
]
[[package]]
name = "safe_arch"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f7caad094bd561859bcd467734a720c3c1f5d1f338995351fefe2190c45efed"
dependencies = [
"bytemuck",
]
[[package]]
name = "same-file"
version = "1.0.6"
@ -5119,17 +5030,6 @@ dependencies = [
"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]]
name = "sharded-slab"
version = "0.1.7"
@ -5319,15 +5219,6 @@ dependencies = [
"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]]
name = "spirv"
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"
checksum = "6a28554d13eb5daba527cc1b91b6c341372a0ae45ed277ffb2c6fbc04f319d7e"
dependencies = [
"wide 0.7.33",
"wide",
]
[[package]]
@ -6944,17 +6835,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03"
dependencies = [
"bytemuck",
"safe_arch 0.7.4",
]
[[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",
"safe_arch",
]
[[package]]

View file

@ -15,5 +15,4 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
uuid = { version = "1", features = ["v4"] }
tokio = { version = "1", features = ["full"] }
dioxus-motion = { version = "0.3", default-features = false, features = ["desktop"] }
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.
/// Avoids CSS @keyframes entirely — no Blitz full-scene repaint loop.
/// Instead of animating opacity (Blitz doesn't reliably restyle inline style changes),
/// 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_motion::prelude::*;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
#[derive(Clone, PartialEq)]
pub enum DotState {
@ -18,44 +23,43 @@ pub enum DotState {
#[component]
pub fn PulsingDot(state: DotState, size: Option<String>) -> Element {
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 {
use_effect(move || {
opacity.animate_to(
0.4,
AnimationConfig::new(AnimationMode::Tween(
Tween::new(Duration::from_millis(1000))
))
.with_loop(LoopMode::Alternate),
);
let dim = is_dim.clone();
let updater = use_hook(|| dioxus::dioxus_core::schedule_update());
use_hook(move || {
std::thread::spawn(move || {
loop {
std::thread::sleep(Duration::from_millis(1000));
dim.fetch_xor(true, Ordering::Relaxed); // toggle
updater(); // trigger re-render
}
});
});
}
let (color, shadow) = match state {
DotState::Running => ("var(--ctp-green)", "0 0 6px var(--ctp-green)"),
DotState::Idle => ("var(--ctp-overlay0)", "none"),
DotState::Stalled => ("var(--ctp-peach)", "0 0 4px var(--ctp-peach)"),
DotState::Done => ("var(--ctp-blue)", "none"),
DotState::Error => ("var(--ctp-red)", "0 0 4px var(--ctp-red)"),
let base_class = match state {
DotState::Running => "dot-running",
DotState::Idle => "dot-idle",
DotState::Stalled => "dot-stalled",
DotState::Done => "dot-done",
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 op = if should_pulse { opacity.get_value() } else { 1.0 };
rsx! {
span {
style: "
display: inline-block;
width: {sz};
height: {sz};
border-radius: 50%;
background: {color};
box-shadow: {shadow};
opacity: {op};
flex-shrink: 0;
",
class: "pulsing-dot {base_class} {dim_class}",
style: "width: {sz}; height: {sz};",
}
}
}

View file

@ -257,6 +257,35 @@ body {{
.status-dot.stalled {{
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 {{
background: var(--ctp-red);
}}