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 terminal;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
pub mod command_palette;
|
pub mod command_palette;
|
||||||
|
pub mod pulsing_dot;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ use crate::state::{
|
||||||
};
|
};
|
||||||
use crate::components::agent_pane::AgentPane;
|
use crate::components::agent_pane::AgentPane;
|
||||||
use crate::components::terminal::TerminalArea;
|
use crate::components::terminal::TerminalArea;
|
||||||
|
use crate::components::pulsing_dot::{PulsingDot, DotState};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ProjectBox(
|
pub fn ProjectBox(
|
||||||
|
|
@ -31,7 +32,13 @@ pub fn ProjectBox(
|
||||||
});
|
});
|
||||||
|
|
||||||
let terminal_lines = demo_terminal_lines();
|
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! {
|
rsx! {
|
||||||
div {
|
div {
|
||||||
|
|
@ -40,7 +47,7 @@ pub fn ProjectBox(
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
div { class: "project-header",
|
div { class: "project-header",
|
||||||
div { class: "status-dot {status_class}" }
|
PulsingDot { state: dot_state.clone() }
|
||||||
div { class: "project-name", "{project.name}" }
|
div { class: "project-name", "{project.name}" }
|
||||||
span { class: "provider-badge", "{project.provider}" }
|
span { class: "provider-badge", "{project.provider}" }
|
||||||
div { class: "project-cwd", "{project.cwd}" }
|
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).
|
/// Mirrors the Svelte app's StatusBar.svelte (Mission Control bar).
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use crate::components::pulsing_dot::{PulsingDot, DotState};
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct FleetState {
|
pub struct FleetState {
|
||||||
|
|
@ -22,20 +23,14 @@ pub fn StatusBar(fleet: FleetState) -> Element {
|
||||||
div { class: "status-bar-left",
|
div { class: "status-bar-left",
|
||||||
// Running
|
// Running
|
||||||
div { class: "status-item",
|
div { class: "status-item",
|
||||||
div {
|
PulsingDot { state: DotState::Running, size: "6px".to_string() }
|
||||||
class: "status-dot running",
|
|
||||||
style: "width: 6px; height: 6px; border-radius: 50%; display: inline-block;",
|
|
||||||
}
|
|
||||||
span { class: "status-count running", "{fleet.running}" }
|
span { class: "status-count running", "{fleet.running}" }
|
||||||
span { "running" }
|
span { "running" }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Idle
|
// Idle
|
||||||
div { class: "status-item",
|
div { class: "status-item",
|
||||||
div {
|
PulsingDot { state: DotState::Idle, size: "6px".to_string() }
|
||||||
class: "status-dot idle",
|
|
||||||
style: "width: 6px; height: 6px; border-radius: 50%; display: inline-block;",
|
|
||||||
}
|
|
||||||
span { class: "status-count idle", "{fleet.idle}" }
|
span { class: "status-count idle", "{fleet.idle}" }
|
||||||
span { "idle" }
|
span { "idle" }
|
||||||
}
|
}
|
||||||
|
|
@ -43,10 +38,7 @@ pub fn StatusBar(fleet: FleetState) -> Element {
|
||||||
// Stalled
|
// Stalled
|
||||||
if fleet.stalled > 0 {
|
if fleet.stalled > 0 {
|
||||||
div { class: "status-item",
|
div { class: "status-item",
|
||||||
div {
|
PulsingDot { state: DotState::Stalled, size: "6px".to_string() }
|
||||||
class: "status-dot stalled",
|
|
||||||
style: "width: 6px; height: 6px; border-radius: 50%; display: inline-block;",
|
|
||||||
}
|
|
||||||
span { class: "status-count stalled", "{fleet.stalled}" }
|
span { class: "status-count stalled", "{fleet.stalled}" }
|
||||||
span { "stalled" }
|
span { "stalled" }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue