feat(ui-gpui): custom Element with direct paint_quad() for zero-overhead pulse
This commit is contained in:
parent
713b53ba0c
commit
3383334821
2 changed files with 90 additions and 68 deletions
|
|
@ -1,11 +1,12 @@
|
|||
//! PulsingDot — throttled render-driven animation at ~5fps (not vsync 60fps).
|
||||
//! PulsingDot — custom GPUI Element that paints a single animated quad.
|
||||
//!
|
||||
//! 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.
|
||||
//! Instead of using the vdom (div().bg(color)), we implement Element directly
|
||||
//! and call window.paint_quad() in paint(). Combined with request_animation_frame(),
|
||||
//! GPUI only repaints THIS element's quad — not the full vdom tree.
|
||||
//! The GPU handles the color interpolation via the paint_quad shader.
|
||||
|
||||
use gpui::*;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::theme;
|
||||
|
||||
|
|
@ -18,20 +19,18 @@ pub enum DotStatus {
|
|||
Error,
|
||||
}
|
||||
|
||||
pub struct PulsingDot {
|
||||
pub struct PulsingDotElement {
|
||||
status: DotStatus,
|
||||
size: f32,
|
||||
size: Pixels,
|
||||
start_time: Instant,
|
||||
last_render: Instant,
|
||||
}
|
||||
|
||||
impl PulsingDot {
|
||||
impl PulsingDotElement {
|
||||
pub fn new(status: DotStatus, size: f32) -> Self {
|
||||
Self {
|
||||
status,
|
||||
size,
|
||||
size: px(size),
|
||||
start_time: Instant::now(),
|
||||
last_render: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -49,58 +48,88 @@ impl PulsingDot {
|
|||
}
|
||||
}
|
||||
|
||||
fn current_color(&self) -> Rgba {
|
||||
fn current_color(&self) -> Hsla {
|
||||
let base = self.base_color();
|
||||
if !self.should_pulse() {
|
||||
return base;
|
||||
return base.into();
|
||||
}
|
||||
let elapsed = self.start_time.elapsed().as_secs_f32();
|
||||
let t = 0.5 - 0.5 * (elapsed * std::f32::consts::PI).sin();
|
||||
let bg = theme::SURFACE0;
|
||||
Rgba {
|
||||
let color = Rgba {
|
||||
r: base.r + (bg.r - base.r) * t,
|
||||
g: base.g + (bg.g - base.g) * t,
|
||||
b: base.b + (bg.b - base.b) * t,
|
||||
a: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Start a throttled animation loop using cx.spawn + background timer
|
||||
pub fn start_throttled_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.last_render = Instant::now();
|
||||
cx.notify();
|
||||
});
|
||||
if ok.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}).detach();
|
||||
};
|
||||
color.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for PulsingDot {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
impl IntoElement for PulsingDotElement {
|
||||
type Element = Self;
|
||||
fn into_element(self) -> Self { self }
|
||||
}
|
||||
|
||||
impl Element for PulsingDotElement {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = ();
|
||||
|
||||
fn id(&self) -> Option<ElementId> { None }
|
||||
|
||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { None }
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let layout_id = window.request_layout(
|
||||
Style {
|
||||
size: Size { width: self.size.into(), height: self.size.into() },
|
||||
flex_shrink: 0.0,
|
||||
..Default::default()
|
||||
},
|
||||
[],
|
||||
cx,
|
||||
);
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
()
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
_prepaint: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) {
|
||||
let color = self.current_color();
|
||||
let radius = self.size / 2.0;
|
||||
|
||||
// NO request_animation_frame() — animation driven by spawn timer above
|
||||
// Paint a single rounded quad — this is the ONLY thing that gets drawn
|
||||
window.paint_quad(fill(bounds, color).corner_radii(radius));
|
||||
|
||||
let r = (color.r * 255.0) as u32;
|
||||
let g = (color.g * 255.0) as u32;
|
||||
let b = (color.b * 255.0) as u32;
|
||||
let hex = rgba(r * 0x1000000 + g * 0x10000 + b * 0x100 + 0xFF);
|
||||
|
||||
div()
|
||||
.w(px(self.size))
|
||||
.h(px(self.size))
|
||||
.rounded(px(self.size / 2.0))
|
||||
.bg(hex)
|
||||
.flex_shrink_0()
|
||||
// Schedule next animation frame — GPUI will call paint() again
|
||||
// But this time, ONLY this element gets repainted (not the full vdom)
|
||||
if self.should_pulse() {
|
||||
window.request_animation_frame();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue