feat(ui-gpui): add pulsing dot via GPUI async runtime (entity-scoped repaint)

This commit is contained in:
Hibryda 2026-03-19 07:51:25 +01:00
parent 3e6307ffd0
commit 57e0e3a087
3 changed files with 116 additions and 2 deletions

View file

@ -0,0 +1,95 @@
//! PulsingDot — GPU-native smooth pulse using GPUI's async runtime.
//!
//! Uses cx.spawn() + background_executor().timer() + entity.update() to
//! smoothly cycle through opacity steps. GPUI only repaints the dirty view.
use gpui::*;
use std::time::Duration;
use crate::theme;
#[derive(Clone, Copy, PartialEq)]
pub enum DotStatus {
Running,
Idle,
Stalled,
Done,
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,
}
impl PulsingDot {
pub fn new(status: DotStatus, size: f32) -> Self {
Self {
status,
step: 0,
size,
}
}
fn should_pulse(&self) -> bool {
matches!(self.status, DotStatus::Running | DotStatus::Stalled)
}
fn base_color(&self) -> Rgba {
match self.status {
DotStatus::Running => theme::GREEN,
DotStatus::Idle => theme::OVERLAY0,
DotStatus::Stalled => theme::PEACH,
DotStatus::Done => theme::BLUE,
DotStatus::Error => theme::RED,
}
}
/// Start the pulse animation. Call from Context after entity creation.
pub fn start_animation(&self, cx: &mut Context<Self>) {
if !self.should_pulse() {
return;
}
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();
}
}
impl Render for PulsingDot {
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 r = (base.r * 255.0) as u32;
let g = (base.g * 255.0) as u32;
let b = (base.b * 255.0) as u32;
let a = (alpha * 255.0) as u32;
let color = rgba(r * 0x1000000 + g * 0x10000 + b * 0x100 + a);
div()
.w(px(self.size))
.h(px(self.size))
.rounded(px(self.size / 2.0))
.bg(color)
.flex_shrink_0()
}
}