//! 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) { if !self.should_pulse() { return; } let epoch = self.next_epoch(); self.schedule_blink(epoch, cx); } fn schedule_blink(&self, epoch: usize, cx: &mut Context) { cx.spawn(async move |this: WeakEntity, 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) -> 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() } }