Skip to main content

guise/
skeleton.rs

1//! `Skeleton` — an animated loading placeholder.
2
3use 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/// A pulsing placeholder block.
29#[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  /// Render a circle of `size` (overrides width/height/radius).
63  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}