diff --git a/ui-dioxus/src/components/pulsing_dot.rs b/ui-dioxus/src/components/pulsing_dot.rs index 3436412..151296c 100644 --- a/ui-dioxus/src/components/pulsing_dot.rs +++ b/ui-dioxus/src/components/pulsing_dot.rs @@ -1,8 +1,8 @@ -/// PulsingDot — GPU-friendly animated status indicator. +/// PulsingDot — GPU-friendly animated status indicator for Blitz renderer. /// -/// Uses Dioxus use_resource + channel pattern for Blitz-compatible animation. -/// Blitz/wgpu doesn't optimize CSS animations (full-scene repaint every frame). -/// This: OS thread does timing → atomic float → use_resource polls it. +/// Pure OS-thread animation — no async runtime dependency. +/// OS thread updates AtomicU32 + calls schedule_update() to trigger re-render. +/// schedule_update() returns Arc — safe from any thread. use dioxus::prelude::*; use std::sync::atomic::{AtomicU32, Ordering}; @@ -22,13 +22,14 @@ pub enum DotState { pub fn PulsingDot(state: DotState, size: Option) -> Element { let should_pulse = matches!(state, DotState::Running | DotState::Stalled); - // Shared atomic for cross-thread opacity (f32 stored as u32 bits) + // Shared atomic for cross-thread opacity (f32 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 { let ao = atomic_opacity.clone(); - // Spawn OS thread once to drive animation timing + // schedule_update returns Arc — works from OS threads + let updater = use_hook(|| dioxus::dioxus_core::schedule_update()); + use_hook(move || { std::thread::spawn(move || { let steps = 8u32; @@ -43,26 +44,13 @@ pub fn PulsingDot(state: DotState, size: Option) -> Element { 0.4 + (t * 0.6) }; ao.store(f32::to_bits(val), Ordering::Relaxed); + updater(); // Trigger re-render from OS thread 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; - } - } - }); } let (color, shadow) = match state { @@ -74,7 +62,11 @@ pub fn PulsingDot(state: DotState, size: Option) -> Element { }; let sz = size.unwrap_or_else(|| "8px".to_string()); - let op = if should_pulse { *rendered_opacity.read() } else { 1.0 }; + let op = if should_pulse { + f32::from_bits(atomic_opacity.load(Ordering::Relaxed)) + } else { + 1.0 + }; rsx! { span {