ccf_gpui_widgets/widgets/
progress_bar.rs1use std::time::Duration;
34
35use gpui::prelude::*;
36use gpui::*;
37
38use crate::theme::{get_theme_or, Theme};
39
40#[derive(Clone, Debug)]
42pub enum ProgressBarEvent {
43 Complete,
45}
46
47pub struct ProgressBar {
49 value: Option<f64>,
51 min: f64,
52 max: f64,
53 custom_theme: Option<Theme>,
54 show_percentage: bool,
55 label: Option<SharedString>,
56 completed_emitted: bool,
58}
59
60impl EventEmitter<ProgressBarEvent> for ProgressBar {}
61
62impl ProgressBar {
63 pub fn new() -> Self {
65 Self {
66 value: Some(0.0),
67 min: 0.0,
68 max: 1.0,
69 custom_theme: None,
70 show_percentage: false,
71 label: None,
72 completed_emitted: false,
73 }
74 }
75
76 #[must_use]
78 pub fn with_value(mut self, value: f64) -> Self {
79 self.value = Some(value.clamp(self.min, self.max));
80 self
81 }
82
83 #[must_use]
85 pub fn min(mut self, min: f64) -> Self {
86 self.min = min;
87 if let Some(v) = self.value {
88 self.value = Some(v.clamp(self.min, self.max));
89 }
90 self
91 }
92
93 #[must_use]
95 pub fn max(mut self, max: f64) -> Self {
96 self.max = max;
97 if let Some(v) = self.value {
98 self.value = Some(v.clamp(self.min, self.max));
99 }
100 self
101 }
102
103 #[must_use]
105 pub fn indeterminate(mut self) -> Self {
106 self.value = None;
107 self
108 }
109
110 #[must_use]
112 pub fn show_percentage(mut self, show: bool) -> Self {
113 self.show_percentage = show;
114 self
115 }
116
117 #[must_use]
119 pub fn label(mut self, text: impl Into<SharedString>) -> Self {
120 self.label = Some(text.into());
121 self
122 }
123
124 #[must_use]
126 pub fn theme(mut self, theme: Theme) -> Self {
127 self.custom_theme = Some(theme);
128 self
129 }
130
131 pub fn value(&self) -> Option<f64> {
133 self.value
134 }
135
136 pub fn percentage(&self) -> Option<f64> {
138 self.value.map(|v| {
139 if (self.max - self.min).abs() < f64::EPSILON {
140 0.0
141 } else {
142 (v - self.min) / (self.max - self.min)
143 }
144 })
145 }
146
147 pub fn is_complete(&self) -> bool {
149 self.value.is_some_and(|v| (v - self.max).abs() < f64::EPSILON)
150 }
151
152 pub fn is_indeterminate(&self) -> bool {
154 self.value.is_none()
155 }
156
157 pub fn set_value(&mut self, value: f64, cx: &mut Context<Self>) {
159 let clamped = value.clamp(self.min, self.max);
160 let old_value = self.value;
161 self.value = Some(clamped);
162
163 if !self.completed_emitted && (clamped - self.max).abs() < f64::EPSILON {
165 if old_value.is_none_or(|v| (v - self.max).abs() >= f64::EPSILON) {
167 self.completed_emitted = true;
168 cx.emit(ProgressBarEvent::Complete);
169 }
170 }
171
172 cx.notify();
173 }
174
175 pub fn set_indeterminate(&mut self, cx: &mut Context<Self>) {
177 self.value = None;
178 self.completed_emitted = false;
179 cx.notify();
180 }
181
182 pub fn reset(&mut self, cx: &mut Context<Self>) {
184 self.value = Some(self.min);
185 self.completed_emitted = false;
186 cx.notify();
187 }
188}
189
190impl Default for ProgressBar {
191 fn default() -> Self {
192 Self::new()
193 }
194}
195
196impl Render for ProgressBar {
197 fn render(&mut self, _window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
198 let theme = get_theme_or(cx, self.custom_theme.as_ref());
199 let percentage = self.percentage();
200 let show_percentage = self.show_percentage;
201 let label = self.label.clone();
202 let is_indeterminate = self.is_indeterminate();
203
204 let bar_height = 8.0;
206
207 let percentage_text = percentage.map(|p| format!("{:.0}%", p * 100.0));
209
210 let track_element = if is_indeterminate {
212 div()
214 .relative()
215 .flex_1()
216 .h(px(bar_height))
217 .rounded_full()
218 .bg(rgb(theme.bg_input))
219 .overflow_hidden()
220 .child(
221 div()
222 .absolute()
223 .top_0()
224 .bottom_0()
225 .w(relative(0.3))
226 .rounded_full()
227 .bg(rgb(theme.primary))
228 .with_animation(
229 "indeterminate_slide",
230 Animation::new(Duration::from_millis(1500))
231 .repeat(),
232 move |el, delta| {
233 let position = -0.3 + delta * 1.3;
235 el.left(relative(position))
236 },
237 )
238 )
239 } else {
240 let fill_width = percentage.unwrap_or(0.0) as f32;
242
243 div()
244 .relative()
245 .flex_1()
246 .h(px(bar_height))
247 .rounded_full()
248 .bg(rgb(theme.bg_input))
249 .overflow_hidden()
250 .child(
251 div()
252 .h_full()
253 .w(relative(fill_width))
254 .rounded_full()
255 .bg(rgb(theme.primary))
256 )
257 };
258
259 div()
260 .id("ccf_progress_bar")
261 .flex()
262 .flex_col()
263 .gap_1()
264 .w_full()
265 .when_some(label.clone(), |d, text| {
267 d.child(
268 div()
269 .flex()
270 .flex_row()
271 .justify_between()
272 .child(
273 div()
274 .text_sm()
275 .text_color(rgb(theme.text_label))
276 .child(text)
277 )
278 .when(show_percentage && percentage_text.is_some(), |d| {
279 d.child(
280 div()
281 .text_sm()
282 .text_color(rgb(theme.text_muted))
283 .child(percentage_text.clone().unwrap_or_default())
284 )
285 })
286 )
287 })
288 .child(track_element)
290 .when(show_percentage && label.is_none() && percentage_text.is_some(), |d| {
292 d.child(
293 div()
294 .text_sm()
295 .text_color(rgb(theme.text_muted))
296 .text_right()
297 .child(percentage_text.unwrap_or_default())
298 )
299 })
300 }
301}
302