Skip to main content

gpui_component/progress/
progress.rs

1use crate::{ActiveTheme, Sizable, Size, StyledExt};
2use gpui::{
3    Animation, AnimationExt as _, App, Background, ElementId, Hsla, IntoElement, IsZero as _,
4    ParentElement, RenderOnce, SharedString, StyleRefinement, Styled, Window, ease_in_out,
5    prelude::FluentBuilder, px, relative,
6};
7use gpui_base::{
8    Progress as BaseProgress, ProgressIndicator, ProgressTrack, Transition, transition,
9};
10use instant::Duration;
11
12/// A linear horizontal progress bar element.
13#[derive(IntoElement)]
14pub struct Progress {
15    id: ElementId,
16    style: StyleRefinement,
17    color: Option<Hsla>,
18    value: f32,
19    accessibility_label: Option<SharedString>,
20    size: Size,
21    loading: bool,
22}
23
24impl Progress {
25    /// Create a new Progress bar.
26    pub fn new(id: impl Into<ElementId>) -> Self {
27        Self {
28            id: id.into(),
29            value: Default::default(),
30            color: None,
31            accessibility_label: None,
32            style: StyleRefinement::default(),
33            size: Size::default(),
34            loading: false,
35        }
36    }
37
38    /// Enable indeterminate loading animation.
39    ///
40    /// When `loading` is `true`, the `value` is ignored and an infinite
41    /// sliding animation is shown instead.
42    pub fn loading(mut self, loading: bool) -> Self {
43        self.loading = loading;
44        self
45    }
46
47    /// Set the color of the progress bar.
48    pub fn color(mut self, color: impl Into<Hsla>) -> Self {
49        self.color = Some(color.into());
50        self
51    }
52
53    /// Set the percentage value of the progress bar.
54    ///
55    /// The value should be between 0.0 and 100.0.
56    pub fn value(mut self, value: f32) -> Self {
57        self.value = value.clamp(0., 100.);
58        self
59    }
60
61    /// Set the accessible name exposed by the progress indicator.
62    pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
63        self.accessibility_label = Some(label.into());
64        self
65    }
66}
67
68impl Styled for Progress {
69    fn style(&mut self) -> &mut StyleRefinement {
70        &mut self.style
71    }
72}
73
74impl Sizable for Progress {
75    fn with_size(mut self, size: impl Into<Size>) -> Self {
76        self.size = size.into();
77        self
78    }
79}
80
81impl RenderOnce for Progress {
82    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
83        let bg = self
84            .color
85            .map(Background::from)
86            .unwrap_or(cx.theme().tokens.progress_bar.into());
87        let value = self.value;
88        let loading = self.loading;
89        let accessibility_label = self.accessibility_label;
90        let reduce_motion = cx.reduce_motion();
91
92        let radius = self.style.corner_radii.clone();
93        let mut inner_style = StyleRefinement::default();
94        inner_style.corner_radii = radius;
95
96        let (height, pill_radius) = match self.size {
97            Size::XSmall => (px(4.), px(2.)),
98            Size::Small => (px(6.), px(3.)),
99            Size::Medium => (px(8.), px(4.)),
100            Size::Large => (px(10.), px(5.)),
101            Size::Size(s) => (s, s / 2.),
102        };
103        // The bar reads as a pill of half its own height, and squares off with
104        // the rest of the UI when the theme has no radius.
105        let radius = if cx.theme().radius.is_zero() {
106            px(0.)
107        } else {
108            pill_radius
109        };
110
111        let animated_value = transition(
112            (self.id.clone(), "indicator"),
113            value,
114            Transition::new(cx.theme().motion_tokens().duration_normal)
115                .easing(cx.theme().motion_tokens().easing_move.clone()),
116            window,
117            cx,
118        );
119
120        BaseProgress::new(self.id)
121            .value(value)
122            .indeterminate(loading)
123            .when_some(accessibility_label, |this, label| {
124                this.accessibility_label(label)
125            })
126            .w_full()
127            .relative()
128            .h(height)
129            .rounded(radius)
130            .refine_style(&self.style)
131            .child(
132                ProgressTrack::new()
133                    .absolute()
134                    .size_full()
135                    .bg(bg.opacity(0.2))
136                    .rounded(radius)
137                    .refine_style(&inner_style),
138            )
139            .child(
140                ProgressIndicator::new()
141                    .absolute()
142                    .top_0()
143                    .left_0()
144                    .h_full()
145                    .bg(bg)
146                    .rounded(radius)
147                    .refine_style(&inner_style)
148                    .map(|this| {
149                        if loading && !reduce_motion {
150                            this.with_animation(
151                                "progress-loading",
152                                Animation::new(Duration::from_secs(1)).repeat(),
153                                move |this, delta| {
154                                    let start =
155                                        relative(ease_in_out(((delta - 0.5) / 0.5).clamp(0., 1.)));
156                                    let end = relative(ease_in_out(1.0 - delta));
157                                    this.when(delta > 0.5, |this| this.left(start)).right(end)
158                                },
159                            )
160                            .into_any_element()
161                        } else if loading {
162                            this.left(relative(0.325))
163                                .right(relative(0.325))
164                                .into_any_element()
165                        } else {
166                            this.w(relative((animated_value / 100.).clamp(0., 1.)))
167                                .into_any_element()
168                        }
169                    }),
170            )
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn stores_an_explicit_accessibility_label() {
180        let plain = Progress::new("upload");
181        assert_eq!(plain.accessibility_label, None);
182
183        let named = Progress::new("upload").accessibility_label("Upload progress");
184        assert_eq!(
185            named.accessibility_label.as_deref(),
186            Some("Upload progress")
187        );
188    }
189}