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