agent-orchestrator/ui-gpui/src/components/pulsing_dot.rs

99 lines
3 KiB
Rust

//! PulsingDot — Zed-style BlinkManager pattern.
//!
//! Uses cx.spawn() + background_executor().timer() + cx.notify() — exactly
//! how Zed's BlinkManager achieves ~1% CPU cursor blink.
//!
//! Key: spawn must be called AFTER entity is registered (not inside cx.new()).
//! The epoch counter cancels stale timers automatically.
use gpui::*;
use std::time::Duration;
use crate::theme;
#[derive(Clone, Copy, PartialEq)]
pub enum DotStatus {
Running,
Idle,
Stalled,
Done,
Error,
}
pub struct PulsingDot {
status: DotStatus,
size: f32,
visible: bool, // toggles each blink
blink_epoch: usize, // cancels stale timers (Zed pattern)
}
impl PulsingDot {
pub fn new(status: DotStatus, size: f32) -> Self {
Self {
status,
size,
visible: true,
blink_epoch: 0,
}
}
fn should_pulse(&self) -> bool {
matches!(self.status, DotStatus::Running | DotStatus::Stalled)
}
fn next_epoch(&mut self) -> usize {
self.blink_epoch += 1;
self.blink_epoch
}
/// Start blinking. MUST be called after entity is registered (not in cx.new).
/// This is the Zed BlinkManager pattern — recursive spawn with epoch guard.
pub fn start_blinking(&mut self, cx: &mut Context<Self>) {
if !self.should_pulse() {
return;
}
let epoch = self.next_epoch();
self.schedule_blink(epoch, cx);
}
fn schedule_blink(&self, epoch: usize, cx: &mut Context<Self>) {
cx.spawn(async move |this: WeakEntity<Self>, cx: &mut AsyncApp| {
cx.background_executor().timer(Duration::from_millis(500)).await;
this.update(cx, |dot, cx| {
// Epoch guard: if epoch changed (pause/resume), this timer is stale
if dot.blink_epoch == epoch {
dot.visible = !dot.visible;
cx.notify(); // marks ONLY this view dirty
dot.schedule_blink(epoch, cx); // recursive schedule
}
}).ok();
}).detach();
}
fn color(&self) -> Rgba {
match self.status {
DotStatus::Running => if self.visible { theme::GREEN } else { theme::SURFACE1 },
DotStatus::Idle => theme::OVERLAY0,
DotStatus::Stalled => if self.visible { theme::PEACH } else { theme::SURFACE1 },
DotStatus::Done => theme::BLUE,
DotStatus::Error => theme::RED,
}
}
}
impl Render for PulsingDot {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let color = self.color();
let r = (color.r * 255.0) as u32;
let g = (color.g * 255.0) as u32;
let b = (color.b * 255.0) as u32;
let hex = rgba(r * 0x1000000 + g * 0x10000 + b * 0x100 + 0xFF);
div()
.w(px(self.size))
.h(px(self.size))
.rounded(px(self.size / 2.0))
.bg(hex)
.flex_shrink_0()
}
}