1use super::paint::{dot, rounded_bar, stroke_polyline};
5use crate::{color::ColorRole, theme::color};
6use gpui::{prelude::*, *};
7use gpui_base::{Easing, Progress};
8use std::time::Duration;
9
10pub const PROGRESS_THICKNESS: Pixels = px(4.);
12pub const PROGRESS_THICK_THICKNESS: Pixels = px(8.);
13const TRACK_GAP: f32 = 4.;
15const STOP_SIZE: f32 = 4.;
17pub(super) const WAVE_AMPLITUDE: f32 = 3.;
20const DETERMINATE_WAVELENGTH: f32 = 40.;
22const INDETERMINATE_WAVELENGTH: f32 = 20.;
23pub const LINEAR_INDETERMINATE_MS: f32 = 1750.;
25const WAVE_PERIOD: Duration = Duration::from_secs(1);
27
28#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
30pub enum ProgressShape {
31 #[default]
32 Flat,
33 Wavy,
35}
36
37pub fn linear_indeterminate_lines(elapsed_ms: f32) -> [(f32, f32); 2] {
43 let t = if elapsed_ms.is_finite() {
44 elapsed_ms.rem_euclid(LINEAR_INDETERMINATE_MS)
45 } else {
46 0.
47 };
48 let key = |delay: f32, duration: f32, (x1, y1, x2, y2): (f32, f32, f32, f32)| {
49 let progress = ((t - delay) / duration).clamp(0., 1.);
50 Easing::cubic_bezier(x1, y1, x2, y2)
51 .map(|easing| easing.sample(progress))
52 .unwrap_or(progress)
53 };
54 [
55 (
56 key(250., 1000., (0.4, 0., 1., 1.)),
57 key(0., 1000., (0.2, 0., 0.8, 1.)),
58 ),
59 (
60 key(900., 850., (0.1, 0., 0.45, 1.)),
61 key(650., 850., (0., 0., 0.65, 1.)),
62 ),
63 ]
64}
65
66#[derive(gpui::IntoElement)]
74pub struct LinearProgress {
75 id: ElementId,
76 label: Option<SharedString>,
77 value: Option<f32>,
78 thickness: Pixels,
79 shape: ProgressShape,
80 muted: bool,
81 disabled: bool,
82 stop_indicator: bool,
83 buffer: Option<f32>,
84 playback: Option<RepeatAnimationPlayback>,
85}
86
87pub(super) const DISABLED_INK: f32 = 0.38;
89pub(super) const DISABLED_TRACK: f32 = 0.12;
90
91pub(super) fn progress_colors(muted: bool, disabled: bool) -> (Rgba, Rgba) {
93 if disabled {
94 let ink = color(ColorRole::ON_SURFACE);
95 return (ink.opacity(DISABLED_INK), ink.opacity(DISABLED_TRACK));
96 }
97 (
98 color(if muted {
99 ColorRole::OUTLINE
100 } else {
101 ColorRole::PRIMARY
102 }),
103 color(ColorRole::SECONDARY_CONTAINER),
104 )
105}
106
107impl LinearProgress {
108 pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
109 Self::unlabelled(id).label(label)
110 }
111
112 pub(super) fn unlabelled(id: impl Into<ElementId>) -> Self {
113 Self {
114 id: id.into(),
115 label: None,
116 value: None,
117 thickness: PROGRESS_THICKNESS,
118 shape: ProgressShape::Flat,
119 muted: false,
120 disabled: false,
121 stop_indicator: true,
122 buffer: None,
123 playback: None,
124 }
125 }
126
127 pub(super) fn label(mut self, label: impl Into<SharedString>) -> Self {
128 self.label = Some(label.into());
129 self
130 }
131
132 pub fn value(mut self, value: Option<f32>) -> Self {
134 self.value = value;
135 self
136 }
137
138 pub fn thickness(mut self, thickness: Pixels) -> Self {
140 self.thickness = thickness;
141 self
142 }
143
144 pub fn thick(self) -> Self {
146 self.thickness(PROGRESS_THICK_THICKNESS)
147 }
148
149 pub fn shape(mut self, shape: ProgressShape) -> Self {
150 self.shape = shape;
151 self
152 }
153
154 pub fn wavy(self) -> Self {
155 self.shape(ProgressShape::Wavy)
156 }
157
158 pub fn muted(mut self, muted: bool) -> Self {
160 self.muted = muted;
161 self
162 }
163
164 pub fn disabled(mut self, disabled: bool) -> Self {
166 self.disabled = disabled;
167 self
168 }
169
170 pub fn stop_indicator(mut self, show: bool) -> Self {
174 self.stop_indicator = show;
175 self
176 }
177
178 pub fn buffer(mut self, buffer: Option<f32>) -> Self {
181 self.buffer = buffer;
182 self
183 }
184
185 pub fn playback(mut self, playback: RepeatAnimationPlayback) -> Self {
187 self.playback = Some(playback);
188 self
189 }
190}
191
192#[derive(Clone, Copy, Debug)]
194struct BarPaint {
195 thickness: f32,
196 amplitude: f32,
197 wavelength: f32,
198 phase: f32,
200 ink: Rgba,
201 track: Rgba,
202 stop: bool,
203 buffer: f32,
205}
206
207pub(super) fn clip_pieces(pieces: Vec<(f32, f32)>, end: f32) -> Vec<(f32, f32)> {
209 pieces
210 .into_iter()
211 .filter_map(|(a, b)| {
212 let b = b.min(end);
213 (b - a > 0.5).then_some((a, b))
214 })
215 .collect()
216}
217
218#[derive(Clone, Copy, Debug)]
220enum Active {
221 Measured(f32),
222 Lines([(f32, f32); 2]),
223}
224
225pub(super) fn track_pieces(width: f32, active: &[(f32, f32)]) -> Vec<(f32, f32)> {
228 let mut active: Vec<(f32, f32)> = active.iter().copied().filter(|(a, b)| b > a).collect();
229 active.sort_by(|a, b| a.0.total_cmp(&b.0));
230 let mut pieces = Vec::new();
231 let mut start = 0.;
232 let mut start_gap = 0.;
233 for (a, b) in active {
234 let end = a - TRACK_GAP.min(b - a);
235 if end - (start + start_gap) > 0.5 {
236 pieces.push((start + start_gap, end));
237 }
238 start = start.max(b);
239 start_gap = TRACK_GAP.min(b - a);
240 }
241 if width - (start + start_gap) > 0.5 {
242 pieces.push((start + start_gap, width));
243 }
244 pieces
245}
246
247fn paint_bar(bounds: Bounds<Pixels>, active: Active, paint: BarPaint, window: &mut Window) {
248 let width = f32::from(bounds.size.width);
249 let x0 = f32::from(bounds.origin.x);
250 let cy = f32::from(bounds.center().y);
251 let t = paint.thickness;
252 let intervals: Vec<(f32, f32)> = match active {
253 Active::Measured(amount) => vec![(0., width * amount)],
254 Active::Lines(lines) => lines
255 .iter()
256 .map(|&(tail, head)| (width * tail, width * head))
257 .collect(),
258 };
259 for (a, b) in clip_pieces(track_pieces(width, &intervals), width * paint.buffer) {
260 rounded_bar(window, x0 + a, x0 + b, cy, t, paint.track);
261 }
262 if let Active::Measured(amount) = active
263 && paint.stop
264 && amount < 0.995
265 {
266 let size = STOP_SIZE.min(t);
268 dot(
269 window,
270 point(px(x0 + width - t / 2.), px(cy)),
271 size,
272 paint.ink,
273 );
274 }
275 for (a, b) in intervals {
276 if b - a <= 0.5 {
277 continue;
278 }
279 if paint.amplitude <= 0.01 {
280 rounded_bar(window, x0 + a, x0 + b, cy, t, paint.ink);
281 continue;
282 }
283 let (from, to) = (a + t / 2., b - t / 2.);
285 if to <= from {
286 dot(
287 window,
288 point(px(x0 + (a + b) / 2.), px(cy)),
289 t.min(b - a),
290 paint.ink,
291 );
292 continue;
293 }
294 let k = std::f32::consts::TAU / paint.wavelength;
295 let y = |x: f32| cy + paint.amplitude * (k * x - std::f32::consts::TAU * paint.phase).sin();
296 let steps = ((to - from) / 2.).ceil().max(1.) as usize;
297 let points: Vec<Point<Pixels>> = (0..=steps)
298 .map(|i| {
299 let x = from + (to - from) * i as f32 / steps as f32;
300 point(px(x0 + x), px(y(x)))
301 })
302 .collect();
303 stroke_polyline(window, &points, t, paint.ink);
304 dot(window, points[0], t, paint.ink);
305 dot(window, points[steps], t, paint.ink);
306 }
307}
308
309fn bar_canvas(active: Active, paint: BarPaint) -> impl IntoElement {
310 canvas(
311 |_, _, _| {},
312 move |bounds, _, window, _| paint_bar(bounds, active, paint, window),
313 )
314 .size_full()
315}
316
317pub(super) const WAVE_VISIBLE: f32 = 0.01;
320
321pub(super) fn wave_amplitude(amount: f32) -> f32 {
323 if amount > 0.1 && amount < 0.95 {
324 WAVE_AMPLITUDE
325 } else {
326 0.
327 }
328}
329
330impl RenderOnce for LinearProgress {
331 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
332 let wavy = self.shape == ProgressShape::Wavy;
333 let thickness = f32::from(self.thickness).max(1.);
334 let height = if wavy {
335 thickness + 2. * WAVE_AMPLITUDE
336 } else {
337 thickness
338 };
339 let target = self
340 .value
341 .map(|v| if v.is_finite() { v.clamp(0., 1.) } else { 0. });
342 let (ink, track) = progress_colors(self.muted, self.disabled);
343 let buffer = match (target, self.buffer) {
344 (Some(target), Some(buffer)) if buffer.is_finite() => buffer.clamp(target, 1.),
345 _ => 1.,
346 };
347 let mut paint = BarPaint {
348 thickness,
349 amplitude: 0.,
350 wavelength: DETERMINATE_WAVELENGTH,
351 phase: 0.,
352 ink,
353 track,
354 stop: self.stop_indicator,
355 buffer,
356 };
357 let root = Progress::new(self.id.clone())
358 .when_some(self.label, |p, label| p.accessibility_label(label))
359 .indeterminate(target.is_none())
360 .value(target.unwrap_or(0.) * 100.)
361 .w_full()
362 .h(px(height));
363 let id = self.id;
364 let playback = self.playback;
365 match target {
366 Some(target) => {
367 let amount = crate::motion::value(id.clone(), target, window, cx).clamp(0., 1.);
369 if !wavy {
370 return root.child(bar_canvas(Active::Measured(amount), paint));
371 }
372 paint.amplitude = crate::motion::value(
373 (id.clone(), "wave-amplitude"),
374 wave_amplitude(target),
375 window,
376 cx,
377 )
378 .max(0.);
379 if paint.amplitude <= WAVE_VISIBLE {
380 return root.child(
382 div()
383 .size_full()
384 .child(bar_canvas(Active::Measured(amount), paint)),
385 );
386 }
387 root.child(
388 div()
389 .size_full()
390 .with_animation(id, Animation::new(WAVE_PERIOD).repeat(), move |slot, t| {
391 slot.child(bar_canvas(
392 Active::Measured(amount),
393 BarPaint { phase: t, ..paint },
394 ))
395 })
396 .when_some(playback, |a, p| a.with_repeat_playback(p))
397 .repeat_when_visible(),
398 )
399 }
400 None => {
401 if wavy {
402 paint.amplitude = WAVE_AMPLITUDE;
403 paint.wavelength = INDETERMINATE_WAVELENGTH;
404 }
405 if cx.reduce_motion() {
406 let lines = linear_indeterminate_lines(600.);
408 return root.child(bar_canvas(Active::Lines(lines), paint));
409 }
410 let loop_ms = LINEAR_INDETERMINATE_MS;
411 root.child(
412 div()
413 .size_full()
414 .with_animation(
415 id,
416 Animation::new(Duration::from_secs_f32(loop_ms / 1000.)).repeat(),
417 move |slot, t| {
418 #[cfg(test)]
419 super::tests::record_repeat_phase(t);
420 let elapsed = t * loop_ms;
421 slot.child(bar_canvas(
422 Active::Lines(linear_indeterminate_lines(elapsed)),
423 BarPaint {
424 phase: elapsed / 1000.,
425 ..paint
426 },
427 ))
428 },
429 )
430 .when_some(playback, |a, p| a.with_repeat_playback(p))
431 .repeat_when_visible(),
432 )
433 }
434 }
435 }
436}
437
438#[cfg(test)]
439mod linear_tests {
440 use super::*;
441 use core::prelude::v1::test;
442
443 #[test]
444 fn two_lines_follow_the_reference_keyframes() {
445 let at = linear_indeterminate_lines;
446 assert_eq!(at(0.), [(0., 0.), (0., 0.)]);
447 let [(tail, head), second] = at(250.);
449 assert_eq!(tail, 0.);
450 assert!(head > 0.1 && head < 0.5, "{head}");
451 assert_eq!(second, (0., 0.));
452 assert!(at(800.)[1].1 > 0.);
454 assert_eq!(at(1250.)[0], (1., 1.));
455 let end = at(LINEAR_INDETERMINATE_MS - 0.01)[1];
456 assert!(end.1 - end.0 < 0.001, "{end:?}");
457 for ms in (0..1750).step_by(25) {
458 for (tail, head) in at(ms as f32) {
459 assert!((0. ..=1.).contains(&tail) && (0. ..=1.).contains(&head));
460 assert!(head >= tail - 1e-6, "{ms}: {tail} {head}");
461 }
462 }
463 }
464
465 #[test]
466 fn track_pieces_keep_a_4dp_gap_beside_active_lines() {
467 assert_eq!(track_pieces(300., &[(0., 0.)]), vec![(0., 300.)]);
468 assert_eq!(track_pieces(300., &[(0., 150.)]), vec![(154., 300.)]);
469 assert_eq!(
470 track_pieces(300., &[(200., 260.), (30., 90.)]),
471 vec![(0., 26.), (94., 196.), (264., 300.)]
472 );
473 assert_eq!(track_pieces(100., &[(0., 2.)]), vec![(4., 100.)]);
475 assert!(track_pieces(100., &[(0., 100.)]).is_empty());
476 }
477
478 #[test]
479 fn a_buffer_ends_the_track_and_disabled_dims_both_parts() {
480 let pieces = track_pieces(300., &[(0., 90.)]);
481 assert_eq!(clip_pieces(pieces.clone(), 300.), vec![(94., 300.)]);
482 assert_eq!(clip_pieces(pieces.clone(), 210.), vec![(94., 210.)]);
483 assert!(clip_pieces(pieces, 90.).is_empty());
485 let (ink, track) = progress_colors(false, true);
486 let on_surface = color(ColorRole::ON_SURFACE);
487 assert_eq!(ink, on_surface.opacity(DISABLED_INK));
488 assert_eq!(track, on_surface.opacity(DISABLED_TRACK));
489 assert_eq!(progress_colors(true, false).0, color(ColorRole::OUTLINE));
490 }
491
492 #[test]
493 fn the_wave_flattens_near_empty_and_full() {
494 assert_eq!(wave_amplitude(0.1), 0.);
495 assert_eq!(wave_amplitude(0.5), WAVE_AMPLITUDE);
496 assert_eq!(wave_amplitude(0.95), 0.);
497 assert_eq!(WAVE_AMPLITUDE * 2. + 4., 10.);
498 assert_eq!(WAVE_AMPLITUDE * 2. + 8., 14.);
499 }
500}