From 8b5a4daf72479c55344d2508cc18939428e87fb6 Mon Sep 17 00:00:00 2001 From: Hibryda Date: Thu, 19 Mar 2026 07:24:00 +0100 Subject: [PATCH] feat(ui-dioxus): smooth pulse via background-color transition (bounded, not infinite) --- ui-dioxus/src/components/pulsing_dot.rs | 37 ++++++++++--------------- ui-dioxus/src/theme.rs | 20 +++++++------ 2 files changed, 25 insertions(+), 32 deletions(-) diff --git a/ui-dioxus/src/components/pulsing_dot.rs b/ui-dioxus/src/components/pulsing_dot.rs index de036ec..67bc988 100644 --- a/ui-dioxus/src/components/pulsing_dot.rs +++ b/ui-dioxus/src/components/pulsing_dot.rs @@ -1,11 +1,12 @@ -/// PulsingDot — smooth pulse via discrete CSS opacity classes. +/// PulsingDot — smooth pulse via background-color transition in Blitz. /// -/// 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. +/// 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. use dioxus::prelude::*; -use std::sync::atomic::{AtomicU8, Ordering}; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; @@ -21,17 +22,17 @@ pub enum DotState { #[component] pub fn PulsingDot(state: DotState, size: Option) -> Element { let should_pulse = matches!(state, DotState::Running | DotState::Stalled); - let step = use_hook(|| Arc::new(AtomicU8::new(0))); + let is_bright = use_hook(|| Arc::new(AtomicBool::new(true))); if should_pulse { - let s = step.clone(); + let flag = is_bright.clone(); let updater = use_hook(|| dioxus::dioxus_core::schedule_update()); use_hook(move || { std::thread::spawn(move || { loop { - std::thread::sleep(Duration::from_millis(125)); - s.store((s.load(Ordering::Relaxed) + 1) % 8, Ordering::Relaxed); + std::thread::sleep(Duration::from_millis(800)); + flag.fetch_xor(true, Ordering::Relaxed); updater(); } }); @@ -46,27 +47,17 @@ pub fn PulsingDot(state: DotState, size: Option) -> Element { DotState::Error => "dot-error", }; - 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", - } + let brightness = if should_pulse { + if is_bright.load(Ordering::Relaxed) { "dot-bright" } else { "dot-dim" } } else { - "" + "dot-bright" }; let sz = size.unwrap_or_else(|| "8px".to_string()); rsx! { span { - class: "pulsing-dot {base_class} {op_class}", + class: "pulsing-dot {base_class} {brightness}", style: "width: {sz}; height: {sz};", } } diff --git a/ui-dioxus/src/theme.rs b/ui-dioxus/src/theme.rs index 9bf538d..ac8c816 100644 --- a/ui-dioxus/src/theme.rs +++ b/ui-dioxus/src/theme.rs @@ -282,15 +282,17 @@ body {{ background: var(--ctp-red); box-shadow: 0 0 4px var(--ctp-red); }} -/* 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; }} +/* 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; }} .status-dot.error {{ background: var(--ctp-red); }}