gpui_component/progress/
progress.rs1use 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#[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 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 pub fn loading(mut self, loading: bool) -> Self {
43 self.loading = loading;
44 self
45 }
46
47 pub fn color(mut self, color: impl Into<Hsla>) -> Self {
49 self.color = Some(color.into());
50 self
51 }
52
53 pub fn value(mut self, value: f32) -> Self {
57 self.value = value.clamp(0., 100.);
58 self
59 }
60
61 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 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}