Skip to main content

gpui_base/
progress.rs

1use gpui::{
2    AnyElement, App, Div, ElementId, InteractiveElement, Interactivity, IntoElement, ParentElement,
3    RenderOnce, Role, SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Window,
4    div, prelude::FluentBuilder as _,
5};
6use smallvec::SmallVec;
7
8use crate::StyledExt as _;
9
10/// An unstyled linear progress root with controlled value accessibility.
11#[derive(IntoElement)]
12pub struct Progress {
13    base: gpui::Stateful<Div>,
14    style: StyleRefinement,
15    value: f32,
16    indeterminate: bool,
17    accessibility_label: Option<SharedString>,
18    children: SmallVec<[AnyElement; 2]>,
19}
20
21impl Progress {
22    pub fn new(id: impl Into<ElementId>) -> Self {
23        Self {
24            base: div().id(id),
25            style: StyleRefinement::default(),
26            value: 0.,
27            indeterminate: false,
28            accessibility_label: None,
29            children: SmallVec::new(),
30        }
31    }
32
33    /// Sets the controlled percentage value, clamped to `0..=100`.
34    pub fn value(mut self, value: f32) -> Self {
35        self.value = value.clamp(0., 100.);
36        self
37    }
38
39    pub fn indeterminate(mut self, indeterminate: bool) -> Self {
40        self.indeterminate = indeterminate;
41        self
42    }
43
44    pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
45        self.accessibility_label = Some(label.into());
46        self
47    }
48}
49
50impl Styled for Progress {
51    fn style(&mut self) -> &mut StyleRefinement {
52        &mut self.style
53    }
54}
55
56impl ParentElement for Progress {
57    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
58        self.children.extend(elements);
59    }
60}
61
62impl InteractiveElement for Progress {
63    fn interactivity(&mut self) -> &mut Interactivity {
64        self.base.interactivity()
65    }
66}
67
68impl StatefulInteractiveElement for Progress {}
69
70impl RenderOnce for Progress {
71    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
72        self.base
73            .role(Role::ProgressIndicator)
74            .when_some(self.accessibility_label, |this, label| {
75                this.aria_label(label)
76            })
77            .aria_min_numeric_value(0.)
78            .aria_max_numeric_value(100.)
79            .when(!self.indeterminate, |this| {
80                this.aria_numeric_value(self.value as f64)
81            })
82            .children(self.children)
83            .refine_style(&self.style)
84    }
85}
86
87macro_rules! progress_part {
88    ($name:ident, $docs:literal) => {
89        #[doc = $docs]
90        #[derive(IntoElement)]
91        pub struct $name {
92            base: Div,
93            style: StyleRefinement,
94            children: SmallVec<[AnyElement; 1]>,
95        }
96
97        impl $name {
98            pub fn new() -> Self {
99                Self {
100                    base: div(),
101                    style: StyleRefinement::default(),
102                    children: SmallVec::new(),
103                }
104            }
105        }
106
107        impl Styled for $name {
108            fn style(&mut self) -> &mut StyleRefinement {
109                &mut self.style
110            }
111        }
112
113        impl ParentElement for $name {
114            fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
115                self.children.extend(elements);
116            }
117        }
118
119        impl RenderOnce for $name {
120            fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
121                self.base.children(self.children).refine_style(&self.style)
122            }
123        }
124    };
125}
126
127progress_part!(ProgressTrack, "An unstyled progress track.");
128progress_part!(ProgressIndicator, "An unstyled progress indicator.");
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use gpui::{Element as _, accesskit};
134
135    #[gpui::test]
136    fn clamps_and_projects_numeric_accessibility(cx: &mut gpui::TestAppContext) {
137        let window = cx.add_empty_window();
138        window.update(|window, cx| {
139            let mut node = accesskit::Node::new(Role::ProgressIndicator);
140            Progress::new("progress")
141                .value(120.)
142                .render(window, cx)
143                .into_element()
144                .write_a11y_info(&mut node);
145
146            assert_eq!(node.numeric_value(), Some(100.));
147            assert_eq!(node.min_numeric_value(), Some(0.));
148            assert_eq!(node.max_numeric_value(), Some(100.));
149        });
150    }
151
152    #[gpui::test]
153    fn indeterminate_progress_omits_numeric_value(cx: &mut gpui::TestAppContext) {
154        let window = cx.add_empty_window();
155        window.update(|window, cx| {
156            let mut node = accesskit::Node::new(Role::ProgressIndicator);
157            Progress::new("loading")
158                .value(40.)
159                .indeterminate(true)
160                .render(window, cx)
161                .into_element()
162                .write_a11y_info(&mut node);
163
164            assert_eq!(node.numeric_value(), None);
165        });
166    }
167
168    #[gpui::test]
169    fn progress_projects_its_accessible_name(cx: &mut gpui::TestAppContext) {
170        let window = cx.add_empty_window();
171        window.update(|window, cx| {
172            let mut node = accesskit::Node::new(Role::ProgressIndicator);
173            Progress::new("download")
174                .accessibility_label("Downloading release")
175                .render(window, cx)
176                .into_element()
177                .write_a11y_info(&mut node);
178
179            assert_eq!(node.label(), Some("Downloading release"));
180        });
181    }
182}