feat(ui-dioxus): add signal-based PulsingDot animation (Blitz-friendly)
This commit is contained in:
parent
6f9607d1ba
commit
b0547d5c05
4 changed files with 83 additions and 14 deletions
|
|
@ -6,3 +6,4 @@ pub mod agent_pane;
|
|||
pub mod terminal;
|
||||
pub mod settings;
|
||||
pub mod command_palette;
|
||||
pub mod pulsing_dot;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use crate::state::{
|
|||
};
|
||||
use crate::components::agent_pane::AgentPane;
|
||||
use crate::components::terminal::TerminalArea;
|
||||
use crate::components::pulsing_dot::{PulsingDot, DotState};
|
||||
|
||||
#[component]
|
||||
pub fn ProjectBox(
|
||||
|
|
@ -31,7 +32,13 @@ pub fn ProjectBox(
|
|||
});
|
||||
|
||||
let terminal_lines = demo_terminal_lines();
|
||||
let status_class = initial_status.css_class();
|
||||
let dot_state = match initial_status {
|
||||
AgentStatus::Running => DotState::Running,
|
||||
AgentStatus::Idle => DotState::Idle,
|
||||
AgentStatus::Done => DotState::Done,
|
||||
AgentStatus::Stalled => DotState::Stalled,
|
||||
AgentStatus::Error => DotState::Error,
|
||||
};
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
|
|
@ -40,7 +47,7 @@ pub fn ProjectBox(
|
|||
|
||||
// Header
|
||||
div { class: "project-header",
|
||||
div { class: "status-dot {status_class}" }
|
||||
PulsingDot { state: dot_state.clone() }
|
||||
div { class: "project-name", "{project.name}" }
|
||||
span { class: "provider-badge", "{project.provider}" }
|
||||
div { class: "project-cwd", "{project.cwd}" }
|
||||
|
|
|
|||
69
ui-dioxus/src/components/pulsing_dot.rs
Normal file
69
ui-dioxus/src/components/pulsing_dot.rs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/// PulsingDot — GPU-friendly animated status indicator.
|
||||
///
|
||||
/// Uses Dioxus signals + async timer instead of CSS @keyframes.
|
||||
/// Blitz/wgpu doesn't optimize CSS animations (causes full-scene repaint every frame).
|
||||
/// Signal-based approach only repaints the dot element on state change (~2x per cycle).
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum DotState {
|
||||
Running,
|
||||
Idle,
|
||||
Stalled,
|
||||
Done,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn PulsingDot(state: DotState, size: Option<String>) -> Element {
|
||||
let should_pulse = matches!(state, DotState::Running | DotState::Stalled);
|
||||
let mut opacity = use_signal(|| 1.0f32);
|
||||
|
||||
// Spring-style pulse: smoothly interpolate opacity between 1.0 and 0.4
|
||||
if should_pulse {
|
||||
use_future(move || async move {
|
||||
let mut going_down = true;
|
||||
loop {
|
||||
// 8 steps per half-cycle = smooth-ish animation at low repaint cost
|
||||
for step in 0..8 {
|
||||
let t = step as f32 / 7.0;
|
||||
let val = if going_down {
|
||||
1.0 - (t * 0.6) // 1.0 → 0.4
|
||||
} else {
|
||||
0.4 + (t * 0.6) // 0.4 → 1.0
|
||||
};
|
||||
opacity.set(val);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(125)).await;
|
||||
}
|
||||
going_down = !going_down;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 sz = size.unwrap_or_else(|| "8px".to_string());
|
||||
let op = if should_pulse { *opacity.read() } 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;
|
||||
",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
/// Mirrors the Svelte app's StatusBar.svelte (Mission Control bar).
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use crate::components::pulsing_dot::{PulsingDot, DotState};
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct FleetState {
|
||||
|
|
@ -22,20 +23,14 @@ pub fn StatusBar(fleet: FleetState) -> Element {
|
|||
div { class: "status-bar-left",
|
||||
// Running
|
||||
div { class: "status-item",
|
||||
div {
|
||||
class: "status-dot running",
|
||||
style: "width: 6px; height: 6px; border-radius: 50%; display: inline-block;",
|
||||
}
|
||||
PulsingDot { state: DotState::Running, size: "6px".to_string() }
|
||||
span { class: "status-count running", "{fleet.running}" }
|
||||
span { "running" }
|
||||
}
|
||||
|
||||
// Idle
|
||||
div { class: "status-item",
|
||||
div {
|
||||
class: "status-dot idle",
|
||||
style: "width: 6px; height: 6px; border-radius: 50%; display: inline-block;",
|
||||
}
|
||||
PulsingDot { state: DotState::Idle, size: "6px".to_string() }
|
||||
span { class: "status-count idle", "{fleet.idle}" }
|
||||
span { "idle" }
|
||||
}
|
||||
|
|
@ -43,10 +38,7 @@ pub fn StatusBar(fleet: FleetState) -> Element {
|
|||
// Stalled
|
||||
if fleet.stalled > 0 {
|
||||
div { class: "status-item",
|
||||
div {
|
||||
class: "status-dot stalled",
|
||||
style: "width: 6px; height: 6px; border-radius: 50%; display: inline-block;",
|
||||
}
|
||||
PulsingDot { state: DotState::Stalled, size: "6px".to_string() }
|
||||
span { class: "status-count stalled", "{fleet.stalled}" }
|
||||
span { "stalled" }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue