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}