1use gpui::{
7 App, IntoElement, ParentElement, RenderOnce, Styled, Window, div, linear_color_stop,
8 linear_gradient, px, relative,
9};
10use gpui_kit_semantics::{NodeSpec, Role, Semantic};
11use gpui_kit_theme::ActiveTheme;
12
13use crate::foundation::Ident;
14use crate::motion::{self, AnimationExt as _, MotionSpec};
15
16const PULSE_CELLS: usize = 5;
17const MATRIX_SIDE: usize = 3;
18const SHIMMER_BAND: f32 = 0.35;
21const SHIMMER_ROW_OFFSET: f32 = 0.08;
22
23#[derive(Debug, IntoElement)]
25pub struct PulseLoader {
26 ident: Ident,
27 cell_size: f32,
28 label: Option<gpui::SharedString>,
29}
30
31impl PulseLoader {
32 pub fn new(ident: impl Into<Ident>) -> Self {
33 Self {
34 ident: ident.into(),
35 cell_size: 8.0,
36 label: None,
37 }
38 }
39
40 pub fn label(mut self, label: impl Into<gpui::SharedString>) -> Self {
42 self.label = Some(label.into());
43 self
44 }
45
46 pub fn cell_size(mut self, cell_size: f32) -> Self {
47 self.cell_size = cell_size;
48 self
49 }
50}
51
52impl RenderOnce for PulseLoader {
53 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
54 let theme = cx.theme();
55 let color = theme.colors.accent;
56 let cell_size = self.cell_size;
57 let period = MotionSpec::new(
58 theme.motion.pulse_ms,
59 motion::CubicBezier::new(0.25, 0.1, 0.25, 1.0),
60 );
61 let ident = self.ident.clone();
62 let spec = busy_spec(&self.ident, self.label.clone());
63 div()
64 .flex()
65 .flex_row()
66 .items_center()
67 .gap(px(cell_size / 2.0))
68 .semantic_in(cx, spec)
69 .children((0..PULSE_CELLS).map(move |index| {
70 div()
71 .size(px(cell_size))
72 .flex()
73 .items_center()
74 .justify_center()
75 .child(
76 div()
77 .size(px(cell_size))
78 .rounded(px(cell_size / 4.0))
79 .bg(color)
80 .with_animation(
81 ident.indexed_element_id(index),
82 period.repeating(),
83 move |element, delta| {
84 let phase = motion::staggered_phase(delta, index, 0.0625);
85 let wave = motion::pulse_wave(phase);
86 element
87 .opacity(0.08 + 0.92 * wave)
88 .size(px(cell_size * (0.9 + 0.1 * wave)))
89 },
90 ),
91 )
92 }))
93 }
94}
95
96#[derive(Debug, IntoElement)]
98pub struct GradientSpinner {
99 ident: Ident,
100 cell_size: f32,
101 label: Option<gpui::SharedString>,
102}
103
104impl GradientSpinner {
105 pub fn new(ident: impl Into<Ident>) -> Self {
106 Self {
107 ident: ident.into(),
108 cell_size: 5.0,
109 label: None,
110 }
111 }
112
113 pub fn label(mut self, label: impl Into<gpui::SharedString>) -> Self {
114 self.label = Some(label.into());
115 self
116 }
117
118 pub fn cell_size(mut self, cell_size: f32) -> Self {
119 self.cell_size = cell_size;
120 self
121 }
122}
123
124impl RenderOnce for GradientSpinner {
125 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
126 let colors = cx.theme().colors.loader_gradient;
127 let cell_size = self.cell_size;
128 let period = MotionSpec::new(750, motion::CubicBezier::new(0.25, 0.1, 0.25, 1.0));
129 let ident = self.ident.clone();
130 let spec = busy_spec(&self.ident, self.label.clone());
131 div()
132 .flex()
133 .flex_col()
134 .gap(px(cell_size / 2.0))
135 .semantic_in(cx, spec)
136 .children((0..MATRIX_SIDE).map(move |row| {
137 let ident = ident.clone();
138 div()
139 .flex()
140 .flex_row()
141 .gap(px(cell_size / 2.0))
142 .children((0..MATRIX_SIDE).map(move |column| {
143 let index = row * MATRIX_SIDE + column;
144 let center = (MATRIX_SIDE as f32 - 1.0) / 2.0;
145 let distance =
146 MATRIX_SIDE as f32 - 1.0 - row as f32 + (column as f32 - center).abs();
147 let phase = distance / (MATRIX_SIDE as f32 + center);
148 div()
149 .size(px(cell_size))
150 .rounded_full()
151 .bg(colors[row])
152 .with_animation(
153 ident.indexed_element_id(index),
154 period.repeating(),
155 move |element, delta| {
156 element.opacity(motion::gradient_opacity(delta + phase, 0.1))
157 },
158 )
159 }))
160 }))
161 }
162}
163
164#[derive(Debug, IntoElement)]
166pub struct Skeleton {
167 ident: Ident,
168 rows: usize,
169 row_height: f32,
170 label: Option<gpui::SharedString>,
171}
172
173impl Skeleton {
174 pub fn new(ident: impl Into<Ident>) -> Self {
175 Self {
176 ident: ident.into(),
177 rows: 3,
178 row_height: 28.0,
179 label: None,
180 }
181 }
182
183 pub fn label(mut self, label: impl Into<gpui::SharedString>) -> Self {
184 self.label = Some(label.into());
185 self
186 }
187
188 pub fn rows(mut self, rows: usize) -> Self {
189 self.rows = rows;
190 self
191 }
192
193 pub fn row_height(mut self, row_height: f32) -> Self {
194 self.row_height = row_height;
195 self
196 }
197}
198
199impl RenderOnce for Skeleton {
200 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
201 let theme = cx.theme();
202 let color = theme.colors.hover.opacity(0.28);
203 let highlight = theme.colors.hover.opacity(0.5);
204 let radius = theme.radii.control;
205 let row_height = self.row_height;
206 let period = MotionSpec::new(
207 theme.motion.shimmer_ms,
208 motion::CubicBezier::new(0.25, 0.1, 0.25, 1.0),
209 );
210 let ident = self.ident.clone();
211 let spec = busy_spec(&self.ident, self.label.clone());
212 div()
213 .flex()
214 .flex_col()
215 .gap(px(6.0))
216 .semantic_in(cx, spec)
217 .children((0..self.rows).map(move |index| {
218 div()
219 .h(px(row_height))
220 .rounded(px(radius))
221 .bg(color)
222 .relative()
223 .overflow_hidden()
224 .child(
231 div()
232 .absolute()
233 .top_0()
234 .bottom_0()
235 .flex()
236 .flex_row()
237 .w(relative(SHIMMER_BAND))
238 .child(div().h_full().w(relative(0.5)).bg(linear_gradient(
242 90.0,
243 linear_color_stop(highlight.opacity(0.0), 0.0),
244 linear_color_stop(highlight, 1.0),
245 )))
246 .child(div().h_full().w(relative(0.5)).bg(linear_gradient(
247 90.0,
248 linear_color_stop(highlight, 0.0),
249 linear_color_stop(highlight.opacity(0.0), 1.0),
250 )))
251 .with_animation(
252 ident.indexed_element_id(index),
253 period.repeating(),
254 move |element, delta| {
255 let phase =
256 motion::staggered_phase(delta, index, SHIMMER_ROW_OFFSET);
257 element
258 .left(relative(motion::shimmer_offset(phase, SHIMMER_BAND)))
259 },
260 ),
261 )
262 }))
263 }
264}
265
266fn busy_spec(ident: &Ident, label: Option<gpui::SharedString>) -> NodeSpec {
269 let mut spec = NodeSpec::new(ident.semantic_id(), Role::Progress).busy(true);
270 if let Some(label) = label {
271 spec = spec.text(label);
272 }
273 spec
274}