fix(ui-dioxus): use atomic+thread for Blitz animation (Signal not Send)

This commit is contained in:
Hibryda 2026-03-19 06:45:52 +01:00
parent b0547d5c05
commit 7cea86361a

View file

@ -1,10 +1,13 @@
/// PulsingDot — GPU-friendly animated status indicator. /// PulsingDot — GPU-friendly animated status indicator.
/// ///
/// Uses Dioxus signals + async timer instead of CSS @keyframes. /// Uses Dioxus use_resource + channel pattern for Blitz-compatible animation.
/// Blitz/wgpu doesn't optimize CSS animations (causes full-scene repaint every frame). /// Blitz/wgpu doesn't optimize CSS animations (full-scene repaint every frame).
/// Signal-based approach only repaints the dot element on state change (~2x per cycle). /// This: OS thread does timing → atomic float → use_resource polls it.
use dioxus::prelude::*; use dioxus::prelude::*;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::time::Duration;
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub enum DotState { pub enum DotState {
@ -18,25 +21,46 @@ 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_signal(|| 1.0f32);
// Spring-style pulse: smoothly interpolate opacity between 1.0 and 0.4 // Shared atomic for cross-thread opacity (f32 stored as u32 bits)
let atomic_opacity = use_hook(|| Arc::new(AtomicU32::new(f32::to_bits(1.0))));
let mut rendered_opacity = use_signal(|| 1.0f32);
if should_pulse { if should_pulse {
use_future(move || async move { let ao = atomic_opacity.clone();
let mut going_down = true; // Spawn OS thread once to drive animation timing
loop { use_hook(move || {
// 8 steps per half-cycle = smooth-ish animation at low repaint cost std::thread::spawn(move || {
for step in 0..8 { let steps = 8u32;
let t = step as f32 / 7.0; let step_ms = 125u64;
let val = if going_down { let mut going_down = true;
1.0 - (t * 0.6) // 1.0 → 0.4 loop {
} else { for i in 0..steps {
0.4 + (t * 0.6) // 0.4 → 1.0 let t = i as f32 / (steps - 1) as f32;
}; let val = if going_down {
opacity.set(val); 1.0 - (t * 0.6)
tokio::time::sleep(std::time::Duration::from_millis(125)).await; } else {
0.4 + (t * 0.6)
};
ao.store(f32::to_bits(val), Ordering::Relaxed);
std::thread::sleep(Duration::from_millis(step_ms));
}
going_down = !going_down;
}
});
});
// Poll the atomic value and update signal (runs in Dioxus runtime)
let ao2 = atomic_opacity.clone();
use_future(move || {
let ao3 = ao2.clone();
async move {
loop {
let bits = ao3.load(Ordering::Relaxed);
let val = f32::from_bits(bits);
rendered_opacity.set(val);
tokio::time::sleep(Duration::from_millis(60)).await;
} }
going_down = !going_down;
} }
}); });
} }
@ -50,7 +74,7 @@ pub fn PulsingDot(state: DotState, size: Option<String>) -> Element {
}; };
let sz = size.unwrap_or_else(|| "8px".to_string()); let sz = size.unwrap_or_else(|| "8px".to_string());
let op = if should_pulse { *opacity.read() } else { 1.0 }; let op = if should_pulse { *rendered_opacity.read() } else { 1.0 };
rsx! { rsx! {
span { span {