1use super::{
6 circular::{CircularProgressFrame, circular_indeterminate_frame},
7 linear::{
8 PROGRESS_THICK_THICKNESS, PROGRESS_THICKNESS, ProgressShape, progress_colors,
9 wave_amplitude,
10 },
11 paint::{dot, stroke_polyline},
12};
13use gpui::{prelude::*, *};
14use gpui_base::Progress;
15use std::{f32::consts::TAU, time::Duration};
16
17const CIRCULAR_WAVE_AMPLITUDE: f32 = 2.;
19const CIRCULAR_WAVELENGTH: f32 = 15.;
21const TRACK_GAP: f32 = 4.;
23const INDETERMINATE_MS: f32 = 6000.;
24
25pub fn circular_progress_size(shape: ProgressShape, thickness: Pixels) -> f32 {
27 let thick = f32::from(thickness) - 4.;
28 match shape {
29 ProgressShape::Flat => 40. + thick,
30 ProgressShape::Wavy => 48. + thick,
31 }
32}
33
34#[derive(gpui::IntoElement)]
41pub struct CircularProgress {
42 id: ElementId,
43 label: SharedString,
44 value: Option<f32>,
45 thickness: Pixels,
46 shape: ProgressShape,
47 show_track: Option<bool>,
48 size: Option<f32>,
49 disabled: bool,
50 playback: Option<RepeatAnimationPlayback>,
51}
52
53impl CircularProgress {
54 pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
55 Self {
56 id: id.into(),
57 label: label.into(),
58 value: None,
59 thickness: PROGRESS_THICKNESS,
60 shape: ProgressShape::Flat,
61 show_track: None,
62 size: None,
63 disabled: false,
64 playback: None,
65 }
66 }
67
68 pub fn value(mut self, value: Option<f32>) -> Self {
69 self.value = value;
70 self
71 }
72
73 pub fn thick(mut self) -> Self {
74 self.thickness = PROGRESS_THICK_THICKNESS;
75 self
76 }
77
78 pub fn thickness(mut self, thickness: Pixels) -> Self {
80 self.thickness = thickness;
81 self
82 }
83
84 pub fn size(mut self, size: f32) -> Self {
87 self.size = Some(size);
88 self
89 }
90
91 pub fn disabled(mut self, disabled: bool) -> Self {
93 self.disabled = disabled;
94 self
95 }
96
97 pub fn shape(mut self, shape: ProgressShape) -> Self {
98 self.shape = shape;
99 self
100 }
101
102 pub fn wavy(self) -> Self {
103 self.shape(ProgressShape::Wavy)
104 }
105
106 pub fn show_track(mut self, show: bool) -> Self {
108 self.show_track = Some(show);
109 self
110 }
111
112 pub fn playback(mut self, playback: RepeatAnimationPlayback) -> Self {
114 self.playback = Some(playback);
115 self
116 }
117}
118
119#[derive(Clone, Copy, Debug)]
120struct RingPaint {
121 thickness: f32,
122 amplitude: f32,
123 phase: f32,
125 show_track: bool,
126 ink: Rgba,
127 track: Rgba,
128}
129
130fn ring_radius(size: f32, thickness: f32, wavy: bool) -> f32 {
132 size / 2. - thickness / 2. - if wavy { CIRCULAR_WAVE_AMPLITUDE } else { 0. }
133}
134
135fn paint_ring(
136 bounds: Bounds<Pixels>,
137 frame: CircularProgressFrame,
138 paint: RingPaint,
139 wavy: bool,
140 window: &mut Window,
141) {
142 let size = f32::from(bounds.size.width.min(bounds.size.height));
143 let center = bounds.center();
144 let t = paint.thickness;
145 let radius = ring_radius(size, t, wavy).max(1.);
146 let start = frame.rotation_degrees.to_radians();
147 let sweep = frame.sweep.clamp(0., 1.) * TAU;
148 let gap = if sweep > 0. {
150 ((TRACK_GAP + t) / radius).min(sweep)
151 } else {
152 0.
153 };
154 let at = |angle: f32, r: f32| {
155 point(
156 center.x + px(r * angle.cos()),
157 center.y + px(r * angle.sin()),
158 )
159 };
160 let arc = |from: f32, length: f32, wave: f32| -> Vec<Point<Pixels>> {
161 let steps = ((length * radius / 2.).ceil() as usize).max(2);
162 let waves = (TAU * radius / CIRCULAR_WAVELENGTH).round().max(1.);
163 (0..=steps)
164 .map(|i| {
165 let angle = from + length * i as f32 / steps as f32;
166 let r = radius + wave * (waves * angle - TAU * paint.phase).sin();
167 at(angle, r)
168 })
169 .collect()
170 };
171 if paint.show_track {
172 let length = TAU - sweep - 2. * gap;
173 if sweep <= 0. {
174 stroke_polyline(window, &arc(start, TAU, 0.), t, paint.track);
175 } else if length > 0. {
176 let points = arc(start + sweep + gap, length, 0.);
177 stroke_polyline(window, &points, t, paint.track);
178 dot(window, points[0], t, paint.track);
179 dot(window, *points.last().unwrap(), t, paint.track);
180 }
181 }
182 if sweep > 0. {
183 let points = arc(start, sweep, paint.amplitude);
184 stroke_polyline(window, &points, t, paint.ink);
185 dot(window, points[0], t, paint.ink);
186 dot(window, *points.last().unwrap(), t, paint.ink);
187 }
188}
189
190fn ring_canvas(frame: CircularProgressFrame, paint: RingPaint, wavy: bool) -> impl IntoElement {
191 canvas(
192 |_, _, _| {},
193 move |bounds, _, window, _| paint_ring(bounds, frame, paint, wavy, window),
194 )
195 .size_full()
196}
197
198impl RenderOnce for CircularProgress {
199 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
200 let wavy = self.shape == ProgressShape::Wavy;
201 let box_size = self
202 .size
203 .filter(|size| size.is_finite() && *size > 0.)
204 .unwrap_or_else(|| circular_progress_size(self.shape, self.thickness));
205 let (ink, track) = progress_colors(false, self.disabled);
206 let target = self
207 .value
208 .map(|v| if v.is_finite() { v.clamp(0., 1.) } else { 0. });
209 let mut paint = RingPaint {
210 thickness: f32::from(self.thickness),
211 amplitude: 0.,
212 phase: 0.,
213 show_track: self.show_track.unwrap_or(target.is_some()),
214 ink,
215 track,
216 };
217 let root = Progress::new(self.id.clone())
218 .accessibility_label(self.label)
219 .indeterminate(target.is_none())
220 .value(target.unwrap_or(0.) * 100.)
221 .flex_shrink_0()
222 .size(px(box_size));
223 let (id, playback) = (self.id, self.playback);
224 match target {
225 Some(target) => {
226 let amount = crate::motion::value(id.clone(), target, window, cx).clamp(0., 1.);
227 let frame = CircularProgressFrame::determinate(amount);
228 if !wavy {
229 return root.child(ring_canvas(frame, paint, false));
230 }
231 paint.amplitude = crate::motion::value(
232 (id.clone(), "wave-amplitude"),
233 wave_amplitude(target) / super::linear::WAVE_AMPLITUDE
234 * CIRCULAR_WAVE_AMPLITUDE,
235 window,
236 cx,
237 )
238 .max(0.);
239 if paint.amplitude <= super::linear::WAVE_VISIBLE {
240 return root.child(div().size_full().child(ring_canvas(frame, paint, true)));
242 }
243 root.child(
244 div()
245 .size_full()
246 .with_animation(
247 id,
248 Animation::new(Duration::from_secs(1)).repeat(),
249 move |slot, t| {
250 slot.child(ring_canvas(
251 frame,
252 RingPaint { phase: t, ..paint },
253 true,
254 ))
255 },
256 )
257 .when_some(playback, |a, p| a.with_repeat_playback(p))
258 .repeat_when_visible(),
259 )
260 }
261 None => {
262 if wavy {
263 paint.amplitude = CIRCULAR_WAVE_AMPLITUDE;
264 }
265 if cx.reduce_motion() {
266 return root.child(ring_canvas(circular_indeterminate_frame(0.), paint, wavy));
267 }
268 root.child(
269 div()
270 .size_full()
271 .with_animation(
272 id,
273 Animation::new(Duration::from_secs_f32(INDETERMINATE_MS / 1000.))
274 .repeat(),
275 move |slot, t| {
276 let elapsed = t * INDETERMINATE_MS;
277 slot.child(ring_canvas(
278 circular_indeterminate_frame(elapsed),
279 RingPaint {
281 phase: t * 6.,
282 ..paint
283 },
284 wavy,
285 ))
286 },
287 )
288 .when_some(playback, |a, p| a.with_repeat_playback(p))
289 .repeat_when_visible(),
290 )
291 }
292 }
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use core::prelude::v1::test;
300
301 #[test]
302 fn boxes_follow_the_kit_sizes() {
303 let sizes = [
304 (ProgressShape::Flat, PROGRESS_THICKNESS, 40.),
305 (ProgressShape::Flat, PROGRESS_THICK_THICKNESS, 44.),
306 (ProgressShape::Wavy, PROGRESS_THICKNESS, 48.),
307 (ProgressShape::Wavy, PROGRESS_THICK_THICKNESS, 52.),
308 ];
309 for (shape, thickness, size) in sizes {
310 assert_eq!(circular_progress_size(shape, thickness), size);
311 }
312 assert_eq!(ring_radius(40., 4., false), 18.);
314 assert_eq!(
315 ring_radius(48., 4., true) + CIRCULAR_WAVE_AMPLITUDE + 2.,
316 24.
317 );
318 }
319}