//! Blink state using Arc — zero entity overhead. //! //! The pulsing dot reads from a shared atomic. A background thread toggles it //! every 500ms and calls window.request_animation_frame() via a stored callback. //! No Entity, no cx.notify(), no dispatch tree involvement. use gpui::*; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; use crate::state::AgentStatus; use crate::theme; /// Shared blink state — just an atomic bool. No Entity, no GPUI overhead. pub struct SharedBlink { pub visible: Arc, } impl SharedBlink { pub fn new() -> Self { Self { visible: Arc::new(AtomicBool::new(true)) } } /// Start blinking on a background thread. Calls `cx.notify()` on the /// parent view entity to trigger repaint. pub fn start( &self, parent: &Entity, cx: &mut Context, ) { let visible = self.visible.clone(); let weak = parent.downgrade(); cx.spawn(async move |_weak_parent: WeakEntity, cx: &mut AsyncApp| { loop { cx.background_executor().timer(Duration::from_millis(500)).await; visible.fetch_xor(true, Ordering::Relaxed); // Notify the PARENT view directly — no intermediate entity let ok = weak.update(cx, |_, cx| cx.notify()); if ok.is_err() { break; } } }).detach(); } } /// Render a status dot as an inline div. Reads from SharedBlink atomically. /// No Entity, no dispatch tree node, no dirty propagation. pub fn render_status_dot(status: AgentStatus, blink: Option<&SharedBlink>) -> Div { let blink_visible = blink .map(|b| b.visible.load(Ordering::Relaxed)) .unwrap_or(true); let color = match status { AgentStatus::Running if !blink_visible => theme::SURFACE1, AgentStatus::Running => theme::GREEN, AgentStatus::Idle => theme::OVERLAY0, AgentStatus::Done => theme::BLUE, AgentStatus::Error => theme::RED, }; div() .w(px(8.0)) .h(px(8.0)) .rounded(px(4.0)) .bg(color) .flex_shrink_0() }