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