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