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