Skip to main content

guise/feedback/
loader.rs

1//! `Loader` — an animated busy indicator (pulsing dots or bars).
2
3use std::time::Duration;
4
5use gpui::prelude::*;
6use gpui::{div, pulsating_between, px, Animation, AnimationExt, App, IntoElement, Window};
7
8use crate::devtools::Probed;
9use crate::style::ColorValue;
10use crate::theme::{theme, ColorName, Size};
11
12/// The loader's visual style.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum LoaderVariant {
15    /// Three pulsing dots (the default).
16    Dots,
17    /// Three pulsing vertical bars.
18    Bars,
19}
20
21/// An animated loading indicator.
22#[derive(IntoElement)]
23pub struct Loader {
24    variant: LoaderVariant,
25    size: Size,
26    color: ColorValue,
27}
28
29impl Loader {
30    pub fn new() -> Self {
31        Loader {
32            variant: LoaderVariant::Dots,
33            size: Size::Md,
34            color: ColorValue::Named(ColorName::Blue),
35        }
36    }
37
38    pub fn variant(mut self, variant: LoaderVariant) -> Self {
39        self.variant = variant;
40        self
41    }
42
43    pub fn size(mut self, size: Size) -> Self {
44        self.size = size;
45        self
46    }
47
48    pub fn color(mut self, color: impl Into<ColorValue>) -> Self {
49        self.color = color.into();
50        self
51    }
52
53    fn unit(&self) -> f32 {
54        match self.size {
55            Size::Xs => 6.0,
56            Size::Sm => 8.0,
57            Size::Md => 10.0,
58            Size::Lg => 13.0,
59            Size::Xl => 16.0,
60        }
61    }
62}
63
64impl Default for Loader {
65    fn default() -> Self {
66        Loader::new()
67    }
68}
69
70impl RenderOnce for Loader {
71    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
72        let t = theme(cx);
73        let color = crate::style::solid(t, self.color);
74        let unit = self.unit();
75        let bars = self.variant == LoaderVariant::Bars;
76
77        let dots = (0..3usize).map(move |i| {
78            let phase = i as f32 / 3.0;
79            let pulse = pulsating_between(0.25, 1.0);
80            let animation = Animation::new(Duration::from_millis(900))
81                .repeat()
82                .with_easing(move |delta| pulse((delta + phase) % 1.0));
83
84            let dot = if bars {
85                div()
86                    .w(px(unit * 0.6))
87                    .h(px(unit * 2.4))
88                    .rounded(px(unit * 0.3))
89            } else {
90                div().w(px(unit)).h(px(unit)).rounded(px(unit))
91            }
92            .bg(color);
93
94            dot.with_animation(("guise-loader-unit", i), animation, |dot, delta| {
95                dot.opacity(delta)
96            })
97        });
98
99        div()
100            .flex()
101            .items_center()
102            .gap(px(unit * 0.6))
103            .children(dots)
104            .probe("Loader")
105    }
106}