1#![allow(clippy::unusual_byte_groupings)]
3
4use std::f32::consts::TAU;
5use std::time::Duration;
6
7use crate::motion::StyledSlot;
8use crate::theme::{ActiveTheme, Theme};
9use gpui::{
10 div, px, Animation, AnimationExt as _, App, IntoElement, RenderOnce, SharedString,
11 StyleRefinement, Styled, Window,
12};
13
14use crate::button::ButtonVariant;
15use crate::chrome::{box_shadow, button_chrome};
16
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
19pub enum SkeletonShape {
20 #[default]
21 Text,
22 Avatar,
23 Control,
24}
25
26impl SkeletonShape {
27 fn metrics(self) -> (f32, f32, f32) {
28 match self {
29 Self::Text => (180.0, 12.0, 6.0),
30 Self::Avatar => (32.0, 32.0, 16.0),
31 Self::Control => (280.0, 36.0, 6.0),
32 }
33 }
34}
35
36#[derive(IntoElement)]
42pub struct Skeleton {
43 id: SharedString,
44 shape: SkeletonShape,
45 theme: Option<Theme>,
46 style: StyleRefinement,
47}
48
49impl Skeleton {
50 pub fn new(id: impl Into<SharedString>) -> Self {
51 Self {
52 id: id.into(),
53 shape: SkeletonShape::Text,
54 theme: None,
55 style: StyleRefinement::default(),
56 }
57 }
58
59 pub fn text(id: impl Into<SharedString>) -> Self {
60 Self::new(id)
61 }
62
63 pub fn avatar(id: impl Into<SharedString>) -> Self {
64 Self::new(id).shape(SkeletonShape::Avatar)
65 }
66
67 pub fn control(id: impl Into<SharedString>) -> Self {
68 Self::new(id).shape(SkeletonShape::Control)
69 }
70
71 pub fn shape(mut self, shape: SkeletonShape) -> Self {
72 self.shape = shape;
73 self
74 }
75
76 pub fn theme(mut self, theme: Theme) -> Self {
78 self.theme = Some(theme);
79 self
80 }
81}
82
83impl Styled for Skeleton {
84 fn style(&mut self) -> &mut StyleRefinement {
85 &mut self.style
86 }
87}
88
89impl RenderOnce for Skeleton {
90 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
91 let theme = self.theme.unwrap_or_else(|| cx.theme());
92 let chrome = button_chrome(theme, ButtonVariant::Secondary);
93 let (width, height, radius) = self.shape.metrics();
94
95 let shadows = vec![
96 box_shadow(0., 1., chrome.inset, 0., 0.),
97 box_shadow(0., chrome.shadow_y, chrome.shadow, chrome.shadow_blur, 0.),
98 ];
99
100 div()
101 .w(px(width))
102 .h(px(height))
103 .flex_shrink_0()
104 .rounded(px(radius))
105 .border_1()
106 .border_color(chrome.border)
107 .bg(chrome.bg)
108 .shadow(shadows)
109 .refine_style(&self.style)
110 .with_animation(
111 self.id,
112 Animation::new(Duration::from_millis(1600)).repeat(),
113 |el, delta| {
114 let opacity = 0.8 + 0.2 * (delta * TAU).cos();
116 el.opacity(opacity)
117 },
118 )
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use crate::theme::{paint, Theme};
126
127 #[test]
128 fn presets_match_paper_geometry() {
129 assert_eq!(SkeletonShape::Text.metrics(), (180.0, 12.0, 6.0));
130 assert_eq!(SkeletonShape::Avatar.metrics(), (32.0, 32.0, 16.0));
131 assert_eq!(SkeletonShape::Control.metrics(), (280.0, 36.0, 6.0));
132 }
133
134 #[test]
135 fn secondary_glass_matches_paper() {
136 let light = button_chrome(Theme::light(), ButtonVariant::Secondary);
137 assert_eq!(light.bg, paint(0xFFFFFF_85));
138 assert_eq!(light.border, paint(0xFFFFFF_B8));
139 assert_eq!(light.inset, paint(0xFFFFFF_E6));
140 assert_eq!(light.shadow, paint(0x0F172A_0F));
141
142 let dark = button_chrome(Theme::dark(), ButtonVariant::Secondary);
143 assert_eq!(dark.bg, paint(0xFFFFFF_12));
144 assert_eq!(dark.border, paint(0xFFFFFF_1A));
145 assert_eq!(dark.inset, paint(0xFFFFFF_1F));
146 assert_eq!(dark.shadow, paint(0x000000_2E));
147 }
148}