diff --git a/ui-dioxus/src/components/pulsing_dot.rs b/ui-dioxus/src/components/pulsing_dot.rs index 67bc988..2a727e4 100644 --- a/ui-dioxus/src/components/pulsing_dot.rs +++ b/ui-dioxus/src/components/pulsing_dot.rs @@ -1,12 +1,11 @@ -/// PulsingDot — smooth pulse via background-color transition in Blitz. +/// PulsingDot — smooth pulse via 6 manual color steps. Zero CSS animation overhead. /// -/// Toggles between bright (green/peach) and dim (surface2) every 800ms. -/// CSS `transition: background-color 0.4s ease` smooths the switch. -/// The transition runs for 400ms (~24 repaints), then stops until next toggle. -/// Net cost: ~24 repaints per second spread across two 400ms bursts = ~2% CPU. +/// 6 pre-computed color classes cycled every 200ms = 1.2s full pulse. +/// Each step: 1 class change → 1 Blitz repaint. No CSS transition engine. +/// Cost: 5 repaints/sec × 1 element = unmeasurable CPU. use dioxus::prelude::*; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicU8, Ordering}; use std::sync::Arc; use std::time::Duration; @@ -19,27 +18,30 @@ pub enum DotState { Error, } +const STEP_MS: u64 = 200; +const STEPS: u8 = 6; + #[component] pub fn PulsingDot(state: DotState, size: Option) -> Element { let should_pulse = matches!(state, DotState::Running | DotState::Stalled); - let is_bright = use_hook(|| Arc::new(AtomicBool::new(true))); + let step = use_hook(|| Arc::new(AtomicU8::new(0))); if should_pulse { - let flag = is_bright.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(800)); - flag.fetch_xor(true, Ordering::Relaxed); + std::thread::sleep(Duration::from_millis(STEP_MS)); + s.store((s.load(Ordering::Relaxed) + 1) % STEPS, Ordering::Relaxed); updater(); } }); }); } - let base_class = match state { + let base = match state { DotState::Running => "dot-running", DotState::Idle => "dot-idle", DotState::Stalled => "dot-stalled", @@ -47,17 +49,20 @@ pub fn PulsingDot(state: DotState, size: Option) -> Element { DotState::Error => "dot-error", }; - let brightness = if should_pulse { - if is_bright.load(Ordering::Relaxed) { "dot-bright" } else { "dot-dim" } + let step_class = if should_pulse { + match step.load(Ordering::Relaxed) { + 0 => "dot-s0", 1 => "dot-s1", 2 => "dot-s2", + 3 => "dot-s3", 4 => "dot-s4", _ => "dot-s5", + } } else { - "dot-bright" + "dot-s0" }; let sz = size.unwrap_or_else(|| "8px".to_string()); rsx! { span { - class: "pulsing-dot {base_class} {brightness}", + class: "pulsing-dot {base} {step_class}", style: "width: {sz}; height: {sz};", } } diff --git a/ui-dioxus/src/theme.rs b/ui-dioxus/src/theme.rs index ac8c816..175320b 100644 --- a/ui-dioxus/src/theme.rs +++ b/ui-dioxus/src/theme.rs @@ -282,17 +282,22 @@ body {{ background: var(--ctp-red); box-shadow: 0 0 4px var(--ctp-red); }} -/* PulsingDot: smooth pulse via background-color transition (bounded, not infinite) */ -.pulsing-dot {{ - display: inline-block; - border-radius: 50%; - flex-shrink: 0; - transition: background-color 0.4s ease; -}} -.dot-bright.dot-running {{ background: var(--ctp-green); box-shadow: 0 0 6px var(--ctp-green); }} -.dot-dim.dot-running {{ background: var(--ctp-surface2); box-shadow: none; }} -.dot-bright.dot-stalled {{ background: var(--ctp-peach); box-shadow: 0 0 4px var(--ctp-peach); }} -.dot-dim.dot-stalled {{ background: var(--ctp-surface2); box-shadow: none; }} +/* PulsingDot: 6-step color fade, no CSS transition (zero animation engine overhead) */ +.pulsing-dot {{ display: inline-block; border-radius: 50%; flex-shrink: 0; }} +/* Running: green (#a6e3a1) → surface2 (#585b70) in 6 steps */ +.dot-running.dot-s0 {{ background: #a6e3a1; box-shadow: 0 0 6px #a6e3a1; }} +.dot-running.dot-s1 {{ background: #8ec094; box-shadow: 0 0 3px #8ec094; }} +.dot-running.dot-s2 {{ background: #769d87; box-shadow: none; }} +.dot-running.dot-s3 {{ background: #6a8a7c; box-shadow: none; }} +.dot-running.dot-s4 {{ background: #769d87; box-shadow: none; }} +.dot-running.dot-s5 {{ background: #8ec094; box-shadow: 0 0 3px #8ec094; }} +/* Stalled: peach (#fab387) → surface2 in 6 steps */ +.dot-stalled.dot-s0 {{ background: #fab387; box-shadow: 0 0 4px #fab387; }} +.dot-stalled.dot-s1 {{ background: #d9a07c; box-shadow: none; }} +.dot-stalled.dot-s2 {{ background: #b88d71; box-shadow: none; }} +.dot-stalled.dot-s3 {{ background: #a88068; box-shadow: none; }} +.dot-stalled.dot-s4 {{ background: #b88d71; box-shadow: none; }} +.dot-stalled.dot-s5 {{ background: #d9a07c; box-shadow: none; }} .status-dot.error {{ background: var(--ctp-red); }}