Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:34:54.818Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

circular_progress.rs

232 lines · 8.2 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.