feat(ui-gpui): render-driven pulse via request_animation_frame() (GPUI native)

This commit is contained in:
Hibryda 2026-03-19 07:59:32 +01:00
parent 57e0e3a087
commit 573105eae6
2 changed files with 21 additions and 35 deletions

View file

@ -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
//! smoothly cycle through opacity steps. GPUI only repaints the dirty view.
//! Uses request_animation_frame() during render to schedule next frame.
//! Only runs while the dot is visible. GPUI repaints only dirty views.
use gpui::*;
use std::time::Duration;
use std::time::Instant;
use crate::theme;
@ -17,21 +17,18 @@ pub enum DotStatus {
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 {
status: DotStatus,
step: u8,
size: f32,
start_time: Instant,
}
impl PulsingDot {
pub fn new(status: DotStatus, size: f32) -> Self {
Self {
status,
step: 0,
size,
start_time: Instant::now(),
}
}
@ -49,35 +46,25 @@ impl PulsingDot {
}
}
/// Start the pulse animation. Call from Context after entity creation.
pub fn start_animation(&self, cx: &mut Context<Self>) {
fn current_opacity(&self) -> f32 {
if !self.should_pulse() {
return;
return 1.0;
}
cx.spawn(async move |weak: WeakEntity<Self>, cx: &mut AsyncApp| {
loop {
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();
let elapsed = self.start_time.elapsed().as_secs_f32();
// Sine wave: 2s period, oscillates between 0.4 and 1.0
0.7 + 0.3 * (elapsed * std::f32::consts::PI).sin()
}
}
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 alpha = if self.should_pulse() {
OPACITIES[self.step as usize % 6]
} else {
1.0
};
let alpha = self.current_opacity();
// Schedule next frame if pulsing — this is how GPUI does continuous animation
if self.should_pulse() {
window.request_animation_frame();
}
let r = (base.r * 255.0) as u32;
let g = (base.g * 255.0) as u32;