feat(ui-dioxus): smooth pulse via background-color transition (bounded, not infinite)

This commit is contained in:
Hibryda 2026-03-19 07:24:00 +01:00
parent a1e2a66cd6
commit 8b5a4daf72
2 changed files with 25 additions and 32 deletions

View file

@ -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. /// Toggles between bright (green/peach) and dim (surface2) every 800ms.
/// Uses class attribute changes which Blitz reliably restyles. /// CSS `transition: background-color 0.4s ease` smooths the switch.
/// 8 re-renders/sec — low CPU, visually smooth. /// 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 dioxus::prelude::*;
use std::sync::atomic::{AtomicU8, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@ -21,17 +22,17 @@ 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 step = use_hook(|| Arc::new(AtomicU8::new(0))); let is_bright = use_hook(|| Arc::new(AtomicBool::new(true)));
if should_pulse { if should_pulse {
let s = step.clone(); let flag = is_bright.clone();
let updater = use_hook(|| dioxus::dioxus_core::schedule_update()); let updater = use_hook(|| dioxus::dioxus_core::schedule_update());
use_hook(move || { use_hook(move || {
std::thread::spawn(move || { std::thread::spawn(move || {
loop { loop {
std::thread::sleep(Duration::from_millis(125)); std::thread::sleep(Duration::from_millis(800));
s.store((s.load(Ordering::Relaxed) + 1) % 8, Ordering::Relaxed); flag.fetch_xor(true, Ordering::Relaxed);
updater(); updater();
} }
}); });
@ -46,27 +47,17 @@ pub fn PulsingDot(state: DotState, size: Option<String>) -> Element {
DotState::Error => "dot-error", DotState::Error => "dot-error",
}; };
let op_class = if should_pulse { let brightness = if should_pulse {
let i = step.load(Ordering::Relaxed); if is_bright.load(Ordering::Relaxed) { "dot-bright" } else { "dot-dim" }
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 { } else {
"" "dot-bright"
}; };
let sz = size.unwrap_or_else(|| "8px".to_string()); let sz = size.unwrap_or_else(|| "8px".to_string());
rsx! { rsx! {
span { span {
class: "pulsing-dot {base_class} {op_class}", class: "pulsing-dot {base_class} {brightness}",
style: "width: {sz}; height: {sz};", style: "width: {sz}; height: {sz};",
} }
} }

View file

@ -282,15 +282,17 @@ body {{
background: var(--ctp-red); background: var(--ctp-red);
box-shadow: 0 0 4px var(--ctp-red); box-shadow: 0 0 4px var(--ctp-red);
}} }}
/* Discrete opacity steps for smooth pulse (Blitz can't animate inline styles) */ /* PulsingDot: smooth pulse via background-color transition (bounded, not infinite) */
.dot-op-0 {{ opacity: 1.0; }} .pulsing-dot {{
.dot-op-1 {{ opacity: 0.85; }} display: inline-block;
.dot-op-2 {{ opacity: 0.7; }} border-radius: 50%;
.dot-op-3 {{ opacity: 0.55; }} flex-shrink: 0;
.dot-op-4 {{ opacity: 0.4; }} transition: background-color 0.4s ease;
.dot-op-5 {{ opacity: 0.55; }} }}
.dot-op-6 {{ opacity: 0.7; }} .dot-bright.dot-running {{ background: var(--ctp-green); box-shadow: 0 0 6px var(--ctp-green); }}
.dot-op-7 {{ opacity: 0.85; }} .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 {{ .status-dot.error {{
background: var(--ctp-red); background: var(--ctp-red);
}} }}