Skip to main content

guise/feedback/
loader.rs

1//! `Loader` — an animated busy indicator (pulsing dots or bars).
2
3use std::sync::OnceLock;
4use std::time::{Duration, Instant};
5
6use gpui::prelude::*;
7use gpui::{
8  canvas, point, pulsating_between, px, quad, size, transparent_black, App, BorderStyle, Bounds,
9  IntoElement, Pixels, Window,
10};
11
12use crate::devtools::Probed;
13use crate::frameclock::{request_frame, FrameKind};
14use crate::style::ColorValue;
15use crate::theme::{theme, ColorName, Size};
16
17const FRAME_INTERVAL: Duration = Duration::from_millis(60);
18const CYCLE_SECONDS: f32 = 0.9;
19
20fn animation_start() -> Instant {
21  static START: OnceLock<Instant> = OnceLock::new();
22  *START.get_or_init(Instant::now)
23}
24
25fn request_next_frame(window: &mut Window, cx: &mut App) {
26  request_frame(FrameKind::Continuous, FRAME_INTERVAL, window, cx);
27}
28
29/// The loader's visual style.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum LoaderVariant {
32  /// Three pulsing dots (the default).
33  Dots,
34  /// Three pulsing vertical bars.
35  Bars,
36}
37
38/// An animated loading indicator.
39#[derive(IntoElement)]
40pub struct Loader {
41  variant: LoaderVariant,
42  size: Size,
43  color: ColorValue,
44}
45
46impl Loader {
47  pub fn new() -> Self {
48    Loader {
49      variant: LoaderVariant::Dots,
50      size: Size::Md,
51      color: ColorValue::Named(ColorName::Blue),
52    }
53  }
54
55  pub fn variant(mut self, variant: LoaderVariant) -> Self {
56    self.variant = variant;
57    self
58  }
59
60  pub fn size(mut self, size: Size) -> Self {
61    self.size = size;
62    self
63  }
64
65  pub fn color(mut self, color: impl Into<ColorValue>) -> Self {
66    self.color = color.into();
67    self
68  }
69
70  fn unit(&self) -> f32 {
71    match self.size {
72      Size::Xs => 6.0,
73      Size::Sm => 8.0,
74      Size::Md => 10.0,
75      Size::Lg => 13.0,
76      Size::Xl => 16.0,
77    }
78  }
79}
80
81impl Default for Loader {
82  fn default() -> Self {
83    Loader::new()
84  }
85}
86
87impl RenderOnce for Loader {
88  fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
89    let t = theme(cx);
90    let color = crate::style::solid(t, self.color);
91    let unit = self.unit();
92    let bars = self.variant == LoaderVariant::Bars;
93    let width = if bars { unit * 0.6 } else { unit };
94    let height = if bars { unit * 2.4 } else { unit };
95    let gap = unit * 0.6;
96    let total_width = width * 3.0 + gap * 2.0;
97    let radius = if bars { unit * 0.3 } else { unit };
98
99    canvas(
100      |_, _, _| (),
101      move |bounds: Bounds<Pixels>, _, window, cx| {
102        if !bounds.intersects(&window.content_mask().bounds) {
103          return;
104        }
105        let cycle = animation_start().elapsed().as_secs_f32() / CYCLE_SECONDS;
106        let pulse = pulsating_between(0.25, 1.0);
107        for index in 0..3 {
108          let delta = (cycle + index as f32 / 3.0) % 1.0;
109          let item = Bounds {
110            origin: point(
111              bounds.origin.x + px(index as f32 * (width + gap)),
112              bounds.origin.y,
113            ),
114            size: size(px(width), px(height)),
115          };
116          window.paint_quad(quad(
117            item,
118            px(radius),
119            color.opacity(pulse(delta)),
120            px(0.0),
121            transparent_black(),
122            BorderStyle::default(),
123          ));
124        }
125        request_next_frame(window, cx);
126      },
127    )
128    .w(px(total_width))
129    .h(px(height))
130    .probe("Loader")
131  }
132}