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}