99 lines
3 KiB
Rust
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()
|
|
}
|
|
}
|