feat(ui-gpui): render-driven pulse via request_animation_frame() (GPUI native)
This commit is contained in:
parent
57e0e3a087
commit
573105eae6
2 changed files with 21 additions and 35 deletions
|
|
@ -88,6 +88,7 @@ impl ProjectBox {
|
||||||
|
|
||||||
/// Initialize sub-views. Must be called after the ProjectBox entity is created.
|
/// Initialize sub-views. Must be called after the ProjectBox entity is created.
|
||||||
pub fn init_subviews(&mut self, cx: &mut Context<Self>) {
|
pub fn init_subviews(&mut self, cx: &mut Context<Self>) {
|
||||||
|
eprintln!("[ProjectBox] init_subviews for {}", self.project.name);
|
||||||
// Create pulsing status dot
|
// Create pulsing status dot
|
||||||
let dot_status = match self.project.agent.status {
|
let dot_status = match self.project.agent.status {
|
||||||
AgentStatus::Running => DotStatus::Running,
|
AgentStatus::Running => DotStatus::Running,
|
||||||
|
|
@ -95,10 +96,8 @@ impl ProjectBox {
|
||||||
AgentStatus::Done => DotStatus::Done,
|
AgentStatus::Done => DotStatus::Done,
|
||||||
AgentStatus::Error => DotStatus::Error,
|
AgentStatus::Error => DotStatus::Error,
|
||||||
};
|
};
|
||||||
let status_dot = cx.new(|cx: &mut Context<PulsingDot>| {
|
let status_dot = cx.new(|_cx: &mut Context<PulsingDot>| {
|
||||||
let dot = PulsingDot::new(dot_status, 8.0);
|
PulsingDot::new(dot_status, 8.0)
|
||||||
dot.start_animation(cx);
|
|
||||||
dot
|
|
||||||
});
|
});
|
||||||
self.status_dot = Some(status_dot);
|
self.status_dot = Some(status_dot);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
//! PulsingDot — GPU-native smooth pulse using GPUI's async runtime.
|
//! PulsingDot — GPU-native animation using render-driven frame requests.
|
||||||
//!
|
//!
|
||||||
//! Uses cx.spawn() + background_executor().timer() + entity.update() to
|
//! Uses request_animation_frame() during render to schedule next frame.
|
||||||
//! smoothly cycle through opacity steps. GPUI only repaints the dirty view.
|
//! Only runs while the dot is visible. GPUI repaints only dirty views.
|
||||||
|
|
||||||
use gpui::*;
|
use gpui::*;
|
||||||
use std::time::Duration;
|
use std::time::Instant;
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
|
|
||||||
|
|
@ -17,21 +17,18 @@ pub enum DotStatus {
|
||||||
Error,
|
Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pre-computed opacity values for smooth sine-wave pulse (6 steps)
|
|
||||||
const OPACITIES: [f32; 6] = [1.0, 0.85, 0.6, 0.4, 0.6, 0.85];
|
|
||||||
|
|
||||||
pub struct PulsingDot {
|
pub struct PulsingDot {
|
||||||
status: DotStatus,
|
status: DotStatus,
|
||||||
step: u8,
|
|
||||||
size: f32,
|
size: f32,
|
||||||
|
start_time: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PulsingDot {
|
impl PulsingDot {
|
||||||
pub fn new(status: DotStatus, size: f32) -> Self {
|
pub fn new(status: DotStatus, size: f32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
status,
|
status,
|
||||||
step: 0,
|
|
||||||
size,
|
size,
|
||||||
|
start_time: Instant::now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,35 +46,25 @@ impl PulsingDot {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start the pulse animation. Call from Context after entity creation.
|
fn current_opacity(&self) -> f32 {
|
||||||
pub fn start_animation(&self, cx: &mut Context<Self>) {
|
|
||||||
if !self.should_pulse() {
|
if !self.should_pulse() {
|
||||||
return;
|
return 1.0;
|
||||||
}
|
}
|
||||||
|
let elapsed = self.start_time.elapsed().as_secs_f32();
|
||||||
cx.spawn(async move |weak: WeakEntity<Self>, cx: &mut AsyncApp| {
|
// Sine wave: 2s period, oscillates between 0.4 and 1.0
|
||||||
loop {
|
0.7 + 0.3 * (elapsed * std::f32::consts::PI).sin()
|
||||||
cx.background_executor().timer(Duration::from_millis(200)).await;
|
|
||||||
let ok = weak.update(cx, |dot, cx| {
|
|
||||||
dot.step = (dot.step + 1) % 6;
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
if ok.is_err() {
|
|
||||||
break; // Entity dropped
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).detach();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for PulsingDot {
|
impl Render for PulsingDot {
|
||||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let base = self.base_color();
|
let base = self.base_color();
|
||||||
let alpha = if self.should_pulse() {
|
let alpha = self.current_opacity();
|
||||||
OPACITIES[self.step as usize % 6]
|
|
||||||
} else {
|
// Schedule next frame if pulsing — this is how GPUI does continuous animation
|
||||||
1.0
|
if self.should_pulse() {
|
||||||
};
|
window.request_animation_frame();
|
||||||
|
}
|
||||||
|
|
||||||
let r = (base.r * 255.0) as u32;
|
let r = (base.r * 255.0) as u32;
|
||||||
let g = (base.g * 255.0) as u32;
|
let g = (base.g * 255.0) as u32;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue