1use std::sync::OnceLock;
4use std::time::{Duration, Instant};
5
6use gpui::prelude::*;
7use gpui::{
8 canvas, pulsating_between, px, quad, transparent_black, App, BorderStyle, Bounds, IntoElement,
9 Pixels, Window,
10};
11
12use crate::devtools::Probed;
13use crate::frameclock::{request_frame, FrameKind};
14use crate::theme::{theme, ColorName, Size};
15
16const FRAME_INTERVAL: Duration = Duration::from_millis(60);
17const CYCLE_SECONDS: f32 = 1.1;
18
19fn animation_start() -> Instant {
20 static START: OnceLock<Instant> = OnceLock::new();
21 *START.get_or_init(Instant::now)
22}
23
24fn request_next_frame(window: &mut Window, cx: &mut App) {
25 request_frame(FrameKind::Continuous, FRAME_INTERVAL, window, cx);
26}
27
28#[derive(IntoElement)]
30pub struct Skeleton {
31 width: Option<f32>,
32 height: f32,
33 radius: Size,
34 circle: bool,
35}
36
37impl Skeleton {
38 pub fn new() -> Self {
39 Skeleton {
40 width: None,
41 height: 16.0,
42 radius: Size::Sm,
43 circle: false,
44 }
45 }
46
47 pub fn width(mut self, width: f32) -> Self {
48 self.width = Some(width);
49 self
50 }
51
52 pub fn height(mut self, height: f32) -> Self {
53 self.height = height;
54 self
55 }
56
57 pub fn radius(mut self, radius: Size) -> Self {
58 self.radius = radius;
59 self
60 }
61
62 pub fn circle(mut self, size: f32) -> Self {
64 self.circle = true;
65 self.width = Some(size);
66 self.height = size;
67 self
68 }
69}
70
71impl Default for Skeleton {
72 fn default() -> Self {
73 Skeleton::new()
74 }
75}
76
77impl RenderOnce for Skeleton {
78 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
79 let t = theme(cx);
80 let color = t
81 .color(ColorName::Gray, if t.scheme.is_dark() { 7 } else { 2 })
82 .hsla();
83 let radius = if self.circle {
84 self.height
85 } else {
86 t.radius(self.radius)
87 };
88
89 let mut block = canvas(
90 |_, _, _| (),
91 move |bounds: Bounds<Pixels>, _, window, cx| {
92 if !bounds.intersects(&window.content_mask().bounds) {
93 return;
94 }
95 let cycle = (animation_start().elapsed().as_secs_f32() / CYCLE_SECONDS) % 1.0;
96 let pulse = pulsating_between(0.4, 1.0);
97 window.paint_quad(quad(
98 bounds,
99 px(radius),
100 color.opacity(pulse(cycle)),
101 px(0.0),
102 transparent_black(),
103 BorderStyle::default(),
104 ));
105 request_next_frame(window, cx);
106 },
107 )
108 .h(px(self.height));
109 block = match self.width {
110 Some(width) => block.w(px(width)),
111 None => block.w_full(),
112 };
113 block.probe("Skeleton")
114 }
115}