circular_progress.rs

  1use documented::Documented;
  2use gpui::{Hsla, PathBuilder, canvas, point};
  3use std::f32::consts::PI;
  4
  5use crate::prelude::*;
  6
  7/// A circular progress indicator that displays progress as an arc growing clockwise from the top.
  8#[derive(IntoElement, RegisterComponent, Documented)]
  9pub struct CircularProgress {
 10    value: f32,
 11    max_value: f32,
 12    size: Pixels,
 13    stroke_width: Pixels,
 14    bg_color: Hsla,
 15    progress_color: Hsla,
 16}
 17
 18impl CircularProgress {
 19    pub fn new(value: f32, max_value: f32, size: Pixels, cx: &App) -> Self {
 20        Self {
 21            value,
 22            max_value,
 23            size,
 24            stroke_width: px(4.0),
 25            bg_color: cx.theme().colors().border_variant,
 26            progress_color: cx.theme().status().info,
 27        }
 28    }
 29
 30    /// Sets the current progress value.
 31    pub fn value(mut self, value: f32) -> Self {
 32        self.value = value;
 33        self
 34    }
 35
 36    /// Sets the maximum value for the progress indicator.
 37    pub fn max_value(mut self, max_value: f32) -> Self {
 38        self.max_value = max_value;
 39        self
 40    }
 41
 42    /// Sets the size (diameter) of the circular progress indicator.
 43    pub fn size(mut self, size: Pixels) -> Self {
 44        self.size = size;
 45        self
 46    }
 47
 48    /// Sets the stroke width of the circular progress indicator.
 49    pub fn stroke_width(mut self, stroke_width: Pixels) -> Self {
 50        self.stroke_width = stroke_width;
 51        self
 52    }
 53
 54    /// Sets the background circle color.
 55    pub fn bg_color(mut self, color: Hsla) -> Self {
 56        self.bg_color = color;
 57        self
 58    }
 59
 60    /// Sets the progress arc color.
 61    pub fn progress_color(mut self, color: Hsla) -> Self {
 62        self.progress_color = color;
 63        self
 64    }
 65}
 66
 67impl RenderOnce for CircularProgress {
 68    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
 69        let value = self.value;
 70        let max_value = self.max_value;
 71        let size = self.size;
 72        let bg_color = self.bg_color;
 73        let progress_color = self.progress_color;
 74
 75        canvas(
 76            |_, _, _| {},
 77            move |bounds, _, window, _cx| {
 78                let current_value = value;
 79
 80                let center_x = bounds.origin.x + bounds.size.width / 2.0;
 81                let center_y = bounds.origin.y + bounds.size.height / 2.0;
 82
 83                let stroke_width = self.stroke_width;
 84                let radius = (size / 2.0) - stroke_width;
 85
 86                // Draw background circle (full 360 degrees)
 87                let mut bg_builder = PathBuilder::stroke(stroke_width);
 88
 89                // Start at rightmost point
 90                bg_builder.move_to(point(center_x + radius, center_y));
 91
 92                // Draw full circle using two 180-degree arcs
 93                bg_builder.arc_to(
 94                    point(radius, radius),
 95                    px(0.),
 96                    false,
 97                    true,
 98                    point(center_x - radius, center_y),
 99                );
100                bg_builder.arc_to(
101                    point(radius, radius),
102                    px(0.),
103                    false,
104                    true,
105                    point(center_x + radius, center_y),
106                );
107                bg_builder.close();
108
109                if let Ok(path) = bg_builder.build() {
110                    window.paint_path(path, bg_color);
111                }
112
113                // Draw progress arc if there's any progress
114                let progress = (current_value / max_value).clamp(0.0, 1.0);
115                if progress > 0.0 {
116                    let mut progress_builder = PathBuilder::stroke(stroke_width);
117
118                    // Handle 100% progress as a special case by drawing a full circle
119                    if progress >= 0.999 {
120                        // Start at rightmost point
121                        progress_builder.move_to(point(center_x + radius, center_y));
122
123                        // Draw full circle using two 180-degree arcs
124                        progress_builder.arc_to(
125                            point(radius, radius),
126                            px(0.),
127                            false,
128                            true,
129                            point(center_x - radius, center_y),
130                        );
131                        progress_builder.arc_to(
132                            point(radius, radius),
133                            px(0.),
134                            false,
135                            true,
136                            point(center_x + radius, center_y),
137                        );
138                        progress_builder.close();
139                    } else {
140                        // Start at 12 o'clock (top) position
141                        let start_x = center_x;
142                        let start_y = center_y - radius;
143                        progress_builder.move_to(point(start_x, start_y));
144
145                        // Calculate the end point of the arc based on progress
146                        // Progress sweeps clockwise from -90° (top)
147                        let angle = -PI / 2.0 + (progress * 2.0 * PI);
148                        let end_x = center_x + radius * angle.cos();
149                        let end_y = center_y + radius * angle.sin();
150
151                        // Use large_arc flag when progress > 0.5 (more than 180 degrees)
152                        let large_arc = progress > 0.5;
153
154                        progress_builder.arc_to(
155                            point(radius, radius),
156                            px(0.),
157                            large_arc,
158                            true, // sweep clockwise
159                            point(end_x, end_y),
160                        );
161                    }
162
163                    if let Ok(path) = progress_builder.build() {
164                        window.paint_path(path, progress_color);
165                    }
166                }
167            },
168        )
169        .size(size)
170    }
171}
172
173impl Component for CircularProgress {
174    fn scope() -> ComponentScope {
175        ComponentScope::Status
176    }
177
178    fn description() -> Option<&'static str> {
179        Some(
180            "A circular progress indicator that displays progress as an arc growing clockwise from the top.",
181        )
182    }
183
184    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
185        let max_value = 100.0;
186        let container = || v_flex().items_center().gap_1();
187
188        Some(
189            example_group(vec![single_example(
190                "Examples",
191                h_flex()
192                    .gap_6()
193                    .child(
194                        container()
195                            .child(CircularProgress::new(0.0, max_value, px(48.0), cx))
196                            .child(Label::new("0%").size(LabelSize::Small)),
197                    )
198                    .child(
199                        container()
200                            .child(CircularProgress::new(25.0, max_value, px(48.0), cx))
201                            .child(Label::new("25%").size(LabelSize::Small)),
202                    )
203                    .child(
204                        container()
205                            .child(CircularProgress::new(50.0, max_value, px(48.0), cx))
206                            .child(Label::new("50%").size(LabelSize::Small)),
207                    )
208                    .child(
209                        container()
210                            .child(CircularProgress::new(75.0, max_value, px(48.0), cx))
211                            .child(Label::new("75%").size(LabelSize::Small)),
212                    )
213                    .child(
214                        container()
215                            .child(CircularProgress::new(100.0, max_value, px(48.0), cx))
216                            .child(Label::new("100%").size(LabelSize::Small)),
217                    )
218                    .into_any_element(),
219            )])
220            .into_any_element(),
221        )
222    }
223}