feat(ui-gpui): add pulsing dot via GPUI async runtime (entity-scoped repaint)
This commit is contained in:
parent
3e6307ffd0
commit
57e0e3a087
3 changed files with 116 additions and 2 deletions
|
|
@ -2,6 +2,7 @@ pub mod agent_pane;
|
||||||
pub mod command_palette;
|
pub mod command_palette;
|
||||||
pub mod project_box;
|
pub mod project_box;
|
||||||
pub mod project_grid;
|
pub mod project_grid;
|
||||||
|
pub mod pulsing_dot;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
pub mod sidebar;
|
pub mod sidebar;
|
||||||
pub mod status_bar;
|
pub mod status_bar;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ use gpui::*;
|
||||||
|
|
||||||
use crate::state::{AgentStatus, Project, ProjectTab};
|
use crate::state::{AgentStatus, Project, ProjectTab};
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
|
use crate::components::pulsing_dot::{PulsingDot, DotStatus};
|
||||||
|
|
||||||
// ── Accent Colors by Index ──────────────────────────────────────────
|
// ── Accent Colors by Index ──────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -71,6 +72,8 @@ pub struct ProjectBox {
|
||||||
pub agent_pane: Option<Entity<crate::components::agent_pane::AgentPane>>,
|
pub agent_pane: Option<Entity<crate::components::agent_pane::AgentPane>>,
|
||||||
/// Entity handle for the embedded terminal (Model tab)
|
/// Entity handle for the embedded terminal (Model tab)
|
||||||
pub terminal_view: Option<Entity<crate::terminal::renderer::TerminalView>>,
|
pub terminal_view: Option<Entity<crate::terminal::renderer::TerminalView>>,
|
||||||
|
/// Animated status dot
|
||||||
|
pub status_dot: Option<Entity<PulsingDot>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProjectBox {
|
impl ProjectBox {
|
||||||
|
|
@ -79,11 +82,26 @@ impl ProjectBox {
|
||||||
project,
|
project,
|
||||||
agent_pane: None,
|
agent_pane: None,
|
||||||
terminal_view: None,
|
terminal_view: None,
|
||||||
|
status_dot: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize sub-views. Must be called after the ProjectBox entity is created.
|
/// Initialize sub-views. Must be called after the ProjectBox entity is created.
|
||||||
pub fn init_subviews(&mut self, cx: &mut Context<Self>) {
|
pub fn init_subviews(&mut self, cx: &mut Context<Self>) {
|
||||||
|
// Create pulsing status dot
|
||||||
|
let dot_status = match self.project.agent.status {
|
||||||
|
AgentStatus::Running => DotStatus::Running,
|
||||||
|
AgentStatus::Idle => DotStatus::Idle,
|
||||||
|
AgentStatus::Done => DotStatus::Done,
|
||||||
|
AgentStatus::Error => DotStatus::Error,
|
||||||
|
};
|
||||||
|
let status_dot = cx.new(|cx: &mut Context<PulsingDot>| {
|
||||||
|
let dot = PulsingDot::new(dot_status, 8.0);
|
||||||
|
dot.start_animation(cx);
|
||||||
|
dot
|
||||||
|
});
|
||||||
|
self.status_dot = Some(status_dot);
|
||||||
|
|
||||||
// Create agent pane with demo messages
|
// Create agent pane with demo messages
|
||||||
let agent_pane = cx.new(|_cx| {
|
let agent_pane = cx.new(|_cx| {
|
||||||
crate::components::agent_pane::AgentPane::with_demo_messages()
|
crate::components::agent_pane::AgentPane::with_demo_messages()
|
||||||
|
|
@ -234,8 +252,8 @@ impl Render for ProjectBox {
|
||||||
.rounded(px(2.0))
|
.rounded(px(2.0))
|
||||||
.bg(accent),
|
.bg(accent),
|
||||||
)
|
)
|
||||||
// Status dot
|
// Animated status dot
|
||||||
.child(project_status_dot(status))
|
.children(self.status_dot.clone())
|
||||||
// Project name
|
// Project name
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
|
|
||||||
95
ui-gpui/src/components/pulsing_dot.rs
Normal file
95
ui-gpui/src/components/pulsing_dot.rs
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
//! PulsingDot — GPU-native smooth pulse using GPUI's async runtime.
|
||||||
|
//!
|
||||||
|
//! Uses cx.spawn() + background_executor().timer() + entity.update() to
|
||||||
|
//! smoothly cycle through opacity steps. GPUI only repaints the dirty view.
|
||||||
|
|
||||||
|
use gpui::*;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::theme;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
pub enum DotStatus {
|
||||||
|
Running,
|
||||||
|
Idle,
|
||||||
|
Stalled,
|
||||||
|
Done,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pre-computed opacity values for smooth sine-wave pulse (6 steps)
|
||||||
|
const OPACITIES: [f32; 6] = [1.0, 0.85, 0.6, 0.4, 0.6, 0.85];
|
||||||
|
|
||||||
|
pub struct PulsingDot {
|
||||||
|
status: DotStatus,
|
||||||
|
step: u8,
|
||||||
|
size: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PulsingDot {
|
||||||
|
pub fn new(status: DotStatus, size: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
status,
|
||||||
|
step: 0,
|
||||||
|
size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_pulse(&self) -> bool {
|
||||||
|
matches!(self.status, DotStatus::Running | DotStatus::Stalled)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_color(&self) -> Rgba {
|
||||||
|
match self.status {
|
||||||
|
DotStatus::Running => theme::GREEN,
|
||||||
|
DotStatus::Idle => theme::OVERLAY0,
|
||||||
|
DotStatus::Stalled => theme::PEACH,
|
||||||
|
DotStatus::Done => theme::BLUE,
|
||||||
|
DotStatus::Error => theme::RED,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start the pulse animation. Call from Context after entity creation.
|
||||||
|
pub fn start_animation(&self, cx: &mut Context<Self>) {
|
||||||
|
if !self.should_pulse() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.spawn(async move |weak: WeakEntity<Self>, cx: &mut AsyncApp| {
|
||||||
|
loop {
|
||||||
|
cx.background_executor().timer(Duration::from_millis(200)).await;
|
||||||
|
let ok = weak.update(cx, |dot, cx| {
|
||||||
|
dot.step = (dot.step + 1) % 6;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
if ok.is_err() {
|
||||||
|
break; // Entity dropped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for PulsingDot {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let base = self.base_color();
|
||||||
|
let alpha = if self.should_pulse() {
|
||||||
|
OPACITIES[self.step as usize % 6]
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let r = (base.r * 255.0) as u32;
|
||||||
|
let g = (base.g * 255.0) as u32;
|
||||||
|
let b = (base.b * 255.0) as u32;
|
||||||
|
let a = (alpha * 255.0) as u32;
|
||||||
|
let color = rgba(r * 0x1000000 + g * 0x10000 + b * 0x100 + a);
|
||||||
|
|
||||||
|
div()
|
||||||
|
.w(px(self.size))
|
||||||
|
.h(px(self.size))
|
||||||
|
.rounded(px(self.size / 2.0))
|
||||||
|
.bg(color)
|
||||||
|
.flex_shrink_0()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue