diff --git a/ui-dioxus/src/components/pulsing_dot.rs b/ui-dioxus/src/components/pulsing_dot.rs index e899d32..de036ec 100644 --- a/ui-dioxus/src/components/pulsing_dot.rs +++ b/ui-dioxus/src/components/pulsing_dot.rs @@ -1,13 +1,11 @@ -/// PulsingDot — Blitz-compatible animated status indicator. +/// PulsingDot — smooth pulse via discrete CSS opacity classes. /// -/// 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. +/// 8 opacity steps (1.0 → 0.4 → 1.0) cycled every 125ms = 1 full pulse per 1s. +/// Uses class attribute changes which Blitz reliably restyles. +/// 8 re-renders/sec — low CPU, visually smooth. use dioxus::prelude::*; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicU8, Ordering}; use std::sync::Arc; use std::time::Duration; @@ -23,18 +21,18 @@ pub enum DotState { #[component] pub fn PulsingDot(state: DotState, size: Option) -> Element { let should_pulse = matches!(state, DotState::Running | DotState::Stalled); - let is_dim = use_hook(|| Arc::new(AtomicBool::new(false))); + let step = use_hook(|| Arc::new(AtomicU8::new(0))); if should_pulse { - let dim = is_dim.clone(); + let s = step.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 + std::thread::sleep(Duration::from_millis(125)); + s.store((s.load(Ordering::Relaxed) + 1) % 8, Ordering::Relaxed); + updater(); } }); }); @@ -48,8 +46,18 @@ pub fn PulsingDot(state: DotState, size: Option) -> Element { DotState::Error => "dot-error", }; - let dim_class = if should_pulse && is_dim.load(Ordering::Relaxed) { - "dot-dim" + let op_class = if should_pulse { + let i = step.load(Ordering::Relaxed); + match i { + 0 => "dot-op-0", + 1 => "dot-op-1", + 2 => "dot-op-2", + 3 => "dot-op-3", + 4 => "dot-op-4", + 5 => "dot-op-5", + 6 => "dot-op-6", + _ => "dot-op-7", + } } else { "" }; @@ -58,7 +66,7 @@ pub fn PulsingDot(state: DotState, size: Option) -> Element { rsx! { span { - class: "pulsing-dot {base_class} {dim_class}", + class: "pulsing-dot {base_class} {op_class}", style: "width: {sz}; height: {sz};", } } diff --git a/ui-dioxus/src/theme.rs b/ui-dioxus/src/theme.rs index fcb10d9..9bf538d 100644 --- a/ui-dioxus/src/theme.rs +++ b/ui-dioxus/src/theme.rs @@ -282,10 +282,15 @@ body {{ background: var(--ctp-red); box-shadow: 0 0 4px var(--ctp-red); }} -.dot-dim {{ - background: var(--ctp-surface1); - box-shadow: none; -}} +/* Discrete opacity steps for smooth pulse (Blitz can't animate inline styles) */ +.dot-op-0 {{ opacity: 1.0; }} +.dot-op-1 {{ opacity: 0.85; }} +.dot-op-2 {{ opacity: 0.7; }} +.dot-op-3 {{ opacity: 0.55; }} +.dot-op-4 {{ opacity: 0.4; }} +.dot-op-5 {{ opacity: 0.55; }} +.dot-op-6 {{ opacity: 0.7; }} +.dot-op-7 {{ opacity: 0.85; }} .status-dot.error {{ background: var(--ctp-red); }}