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#[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 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}