fix(ui-dioxus): class-toggle animation instead of inline opacity (Blitz compatible)
This commit is contained in:
parent
67ab77ebf4
commit
2f03cf0ef0
4 changed files with 66 additions and 153 deletions
|
|
@ -1,10 +1,15 @@
|
|||
/// PulsingDot — Blitz-compatible animated status indicator using dioxus-motion.
|
||||
/// PulsingDot — Blitz-compatible animated status indicator.
|
||||
///
|
||||
/// Uses dioxus-motion's Tween with LoopMode::Alternate for infinite ping-pong.
|
||||
/// Avoids CSS @keyframes entirely — no Blitz full-scene repaint loop.
|
||||
/// 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.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_motion::prelude::*;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum DotState {
|
||||
|
|
@ -18,44 +23,43 @@ pub enum DotState {
|
|||
#[component]
|
||||
pub fn PulsingDot(state: DotState, size: Option<String>) -> Element {
|
||||
let should_pulse = matches!(state, DotState::Running | DotState::Stalled);
|
||||
let mut opacity = use_motion(1.0f32);
|
||||
let is_dim = use_hook(|| Arc::new(AtomicBool::new(false)));
|
||||
|
||||
// Infinite ping-pong tween: 1.0 → 0.4 → 1.0 → ...
|
||||
if should_pulse {
|
||||
use_effect(move || {
|
||||
opacity.animate_to(
|
||||
0.4,
|
||||
AnimationConfig::new(AnimationMode::Tween(
|
||||
Tween::new(Duration::from_millis(1000))
|
||||
))
|
||||
.with_loop(LoopMode::Alternate),
|
||||
);
|
||||
let dim = is_dim.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
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let (color, shadow) = match state {
|
||||
DotState::Running => ("var(--ctp-green)", "0 0 6px var(--ctp-green)"),
|
||||
DotState::Idle => ("var(--ctp-overlay0)", "none"),
|
||||
DotState::Stalled => ("var(--ctp-peach)", "0 0 4px var(--ctp-peach)"),
|
||||
DotState::Done => ("var(--ctp-blue)", "none"),
|
||||
DotState::Error => ("var(--ctp-red)", "0 0 4px var(--ctp-red)"),
|
||||
let base_class = match state {
|
||||
DotState::Running => "dot-running",
|
||||
DotState::Idle => "dot-idle",
|
||||
DotState::Stalled => "dot-stalled",
|
||||
DotState::Done => "dot-done",
|
||||
DotState::Error => "dot-error",
|
||||
};
|
||||
|
||||
let dim_class = if should_pulse && is_dim.load(Ordering::Relaxed) {
|
||||
"dot-dim"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let sz = size.unwrap_or_else(|| "8px".to_string());
|
||||
let op = if should_pulse { opacity.get_value() } else { 1.0 };
|
||||
|
||||
rsx! {
|
||||
span {
|
||||
style: "
|
||||
display: inline-block;
|
||||
width: {sz};
|
||||
height: {sz};
|
||||
border-radius: 50%;
|
||||
background: {color};
|
||||
box-shadow: {shadow};
|
||||
opacity: {op};
|
||||
flex-shrink: 0;
|
||||
",
|
||||
class: "pulsing-dot {base_class} {dim_class}",
|
||||
style: "width: {sz}; height: {sz};",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue