Skip to main content

ui/components/
skeleton.rs

1use gpui::{Animation, AnimationExt, pulsating_between};
2use std::time::Duration;
3
4use crate::prelude::*;
5
6/// A pulsing placeholder block shown while content is loading.
7#[derive(IntoElement, RegisterComponent)]
8pub struct Skeleton {
9    id: ElementId,
10    width: Option<DefiniteLength>,
11    height: Option<DefiniteLength>,
12    rounded: bool,
13}
14
15impl Skeleton {
16    pub fn new() -> Self {
17        Self {
18            id: ElementId::Name("skeleton".into()),
19            width: None,
20            height: None,
21            rounded: true,
22        }
23    }
24
25    /// Overrides the element/animation id (defaults to `"skeleton"`). Set
26    /// this to a unique value when rendering multiple `Skeleton`s as
27    /// siblings so each gets its own animation key instead of sharing one.
28    pub fn id(mut self, id: impl Into<ElementId>) -> Self {
29        self.id = id.into();
30        self
31    }
32
33    pub fn width(mut self, width: impl Into<DefiniteLength>) -> Self {
34        self.width = Some(width.into());
35        self
36    }
37
38    pub fn height(mut self, height: impl Into<DefiniteLength>) -> Self {
39        self.height = Some(height.into());
40        self
41    }
42
43    pub fn rounded(mut self, rounded: bool) -> Self {
44        self.rounded = rounded;
45        self
46    }
47}
48
49impl Default for Skeleton {
50    fn default() -> Self {
51        Self::new()
52    }
53}
54
55impl RenderOnce for Skeleton {
56    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
57        let muted = semantic::muted_bg(cx);
58
59        let id = self.id;
60
61        div()
62            .id(id.clone())
63            .map(|this| {
64                if let Some(width) = self.width {
65                    this.w(width)
66                } else {
67                    this.w_full()
68                }
69            })
70            .h(self.height.unwrap_or_else(|| rems(1.).into()))
71            .when(self.rounded, |this| this.rounded_md())
72            .bg(muted)
73            .with_animation(
74                id,
75                Animation::new(Duration::from_millis(1500))
76                    .repeat()
77                    .with_easing(pulsating_between(0.4, 1.0)),
78                |el, delta| el.opacity(delta),
79            )
80    }
81}
82
83impl Component for Skeleton {
84    fn scope() -> ComponentScope {
85        ComponentScope::Loading
86    }
87
88    fn description() -> Option<&'static str> {
89        Some("A pulsing placeholder block shown while content is loading.")
90    }
91
92    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
93        Some(
94            v_flex()
95                .gap_3()
96                .w(px(320.))
97                .child(Skeleton::new().height(rems(1.)))
98                .child(Skeleton::new().width(relative(0.75)).height(rems(1.)))
99                .child(Skeleton::new().width(relative(0.5)).height(rems(1.)))
100                .into_any_element(),
101        )
102    }
103}