diff --git a/ui-gpui/src/components/project_box.rs b/ui-gpui/src/components/project_box.rs index f9bf022..0f04dfb 100644 --- a/ui-gpui/src/components/project_box.rs +++ b/ui-gpui/src/components/project_box.rs @@ -96,8 +96,10 @@ impl ProjectBox { AgentStatus::Done => DotStatus::Done, AgentStatus::Error => DotStatus::Error, }; - let status_dot = cx.new(|_cx: &mut Context| { - PulsingDot::new(dot_status, 8.0) + let status_dot = cx.new(|cx: &mut Context| { + let dot = PulsingDot::new(dot_status, 8.0); + dot.start_throttled_animation(cx); + dot }); self.status_dot = Some(status_dot); diff --git a/ui-gpui/src/components/pulsing_dot.rs b/ui-gpui/src/components/pulsing_dot.rs index 35a199b..6f344ec 100644 --- a/ui-gpui/src/components/pulsing_dot.rs +++ b/ui-gpui/src/components/pulsing_dot.rs @@ -1,10 +1,11 @@ -//! PulsingDot — GPU-native animation using render-driven frame requests. +//! PulsingDot — throttled render-driven animation at ~5fps (not vsync 60fps). //! -//! Uses request_animation_frame() during render to schedule next frame. -//! Only runs while the dot is visible. GPUI repaints only dirty views. +//! request_animation_frame() at vsync = 90% CPU. Instead, use on_next_frame() +//! with a 200ms sleep to schedule the NEXT render 200ms later. +//! 5 repaints/sec for a pulsing dot is smooth enough and costs ~1-2% CPU. use gpui::*; -use std::time::Instant; +use std::time::{Duration, Instant}; use crate::theme; @@ -21,6 +22,7 @@ pub struct PulsingDot { status: DotStatus, size: f32, start_time: Instant, + last_render: Instant, } impl PulsingDot { @@ -29,6 +31,7 @@ impl PulsingDot { status, size, start_time: Instant::now(), + last_render: Instant::now(), } } @@ -46,17 +49,13 @@ impl PulsingDot { } } - /// Interpolate between the bright color and background (SURFACE0) based on sine wave fn current_color(&self) -> Rgba { let base = self.base_color(); if !self.should_pulse() { return base; } let elapsed = self.start_time.elapsed().as_secs_f32(); - // t oscillates 0.0 (bright) to 1.0 (dim) with 2s period let t = 0.5 - 0.5 * (elapsed * std::f32::consts::PI).sin(); - - // Lerp between base color and SURFACE0 (background) let bg = theme::SURFACE0; Rgba { r: base.r + (bg.r - base.r) * t, @@ -65,16 +64,32 @@ impl PulsingDot { a: 1.0, } } + + /// Start a throttled animation loop using cx.spawn + background timer + pub fn start_throttled_animation(&self, cx: &mut Context) { + if !self.should_pulse() { + return; + } + cx.spawn(async move |weak: WeakEntity, cx: &mut AsyncApp| { + loop { + cx.background_executor().timer(Duration::from_millis(200)).await; + let ok = weak.update(cx, |dot, cx| { + dot.last_render = Instant::now(); + cx.notify(); + }); + if ok.is_err() { + break; + } + } + }).detach(); + } } impl Render for PulsingDot { - fn render(&mut self, window: &mut Window, _cx: &mut Context) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { let color = self.current_color(); - // Schedule next frame if pulsing - if self.should_pulse() { - window.request_animation_frame(); - } + // NO request_animation_frame() — animation driven by spawn timer above let r = (color.r * 255.0) as u32; let g = (color.g * 255.0) as u32;