armas_basic/components/
progress.rs1use crate::ext::ArmasContextExt;
9use egui::{Color32, Pos2, Ui, Vec2};
10use std::f32::consts::PI;
11
12const PROGRESS_HEIGHT: f32 = 8.0; const PROGRESS_CORNER_RADIUS: f32 = 9999.0; const CIRCULAR_SIZE: f32 = 48.0;
16const CIRCULAR_STROKE: f32 = 4.0;
17
18pub struct Progress {
37 value: f32,
39 width: Option<f32>,
41 height: f32,
43}
44
45impl Progress {
46 #[must_use]
51 pub const fn new(value: f32) -> Self {
52 Self {
53 value: value.clamp(0.0, 100.0),
54 width: None,
55 height: PROGRESS_HEIGHT,
56 }
57 }
58
59 #[must_use]
61 pub const fn width(mut self, width: f32) -> Self {
62 self.width = Some(width);
63 self
64 }
65
66 #[must_use]
68 pub const fn height(mut self, height: f32) -> Self {
69 self.height = height;
70 self
71 }
72
73 pub fn show(self, ui: &mut Ui) -> egui::Response {
75 let theme = ui.ctx().armas_theme();
76 let desired_width = self.width.unwrap_or_else(|| ui.available_width());
77 let corner_radius = PROGRESS_CORNER_RADIUS.min(self.height / 2.0);
78
79 let (rect, response) =
80 ui.allocate_exact_size(Vec2::new(desired_width, self.height), egui::Sense::hover());
81
82 if ui.is_rect_visible(rect) {
83 let primary = theme.primary();
85 let track_color = Color32::from_rgba_unmultiplied(
86 primary.r(),
87 primary.g(),
88 primary.b(),
89 51, );
91
92 ui.painter().rect_filled(rect, corner_radius, track_color);
93
94 let progress_fraction = self.value / 100.0;
96 let fill_width = rect.width() * progress_fraction;
97
98 if fill_width > 0.0 {
99 let fill_rect =
100 egui::Rect::from_min_size(rect.min, Vec2::new(fill_width, self.height));
101
102 ui.painter().rect_filled(fill_rect, corner_radius, primary);
103 }
104 }
105
106 response
107 }
108}
109
110pub struct CircularProgressBar {
134 value: Option<f32>,
136 size: f32,
138 stroke_width: f32,
140 show_percentage: bool,
142 rotation: f32,
144}
145
146impl CircularProgressBar {
147 #[must_use]
152 pub const fn new(value: f32) -> Self {
153 Self {
154 value: Some(value.clamp(0.0, 100.0)),
155 size: CIRCULAR_SIZE,
156 stroke_width: CIRCULAR_STROKE,
157 show_percentage: false,
158 rotation: 0.0,
159 }
160 }
161
162 #[must_use]
164 pub const fn indeterminate() -> Self {
165 Self {
166 value: None,
167 size: CIRCULAR_SIZE,
168 stroke_width: CIRCULAR_STROKE,
169 show_percentage: false,
170 rotation: 0.0,
171 }
172 }
173
174 #[must_use]
176 pub const fn size(mut self, size: f32) -> Self {
177 self.size = size;
178 self
179 }
180
181 #[must_use]
183 pub const fn stroke_width(mut self, width: f32) -> Self {
184 self.stroke_width = width;
185 self
186 }
187
188 #[must_use]
190 pub const fn show_percentage(mut self, show: bool) -> Self {
191 self.show_percentage = show;
192 self
193 }
194
195 pub fn show(mut self, ui: &mut Ui) -> egui::Response {
197 let theme = ui.ctx().armas_theme();
198 let (rect, response) = ui.allocate_exact_size(Vec2::splat(self.size), egui::Sense::hover());
199
200 if ui.is_rect_visible(rect) {
201 let center = rect.center();
202 let radius = (self.size - self.stroke_width) / 2.0;
203 let primary = theme.primary();
204
205 let track_color =
207 Color32::from_rgba_unmultiplied(primary.r(), primary.g(), primary.b(), 51);
208
209 ui.painter().circle_stroke(
210 center,
211 radius,
212 egui::Stroke::new(self.stroke_width, track_color),
213 );
214
215 if let Some(value) = self.value {
216 let progress_fraction = value / 100.0;
218 let arc_angle = progress_fraction * 2.0 * PI;
219 self.draw_arc(ui, center, radius, -PI / 2.0, arc_angle, primary);
220
221 if self.show_percentage {
223 let percentage = value as u32;
224 ui.painter().text(
225 center,
226 egui::Align2::CENTER_CENTER,
227 format!("{percentage}%"),
228 egui::FontId::proportional(self.size * 0.25),
229 theme.foreground(),
230 );
231 }
232 } else {
233 let dt = ui.input(|i| i.stable_dt);
235 self.rotation += dt * 3.0;
236 self.rotation %= 2.0 * PI;
237
238 let breath_phase = (self.rotation * 2.0).sin() * 0.5 + 0.5;
240 let arc_len = PI / 4.0 + breath_phase * PI / 2.0;
241
242 self.draw_arc(ui, center, radius, self.rotation, arc_len, primary);
243
244 ui.ctx().request_repaint();
245 }
246 }
247
248 response
249 }
250
251 fn draw_arc(
253 &self,
254 ui: &mut Ui,
255 center: Pos2,
256 radius: f32,
257 start_angle: f32,
258 arc_length: f32,
259 color: Color32,
260 ) {
261 let segments = 32;
262 let angle_step = arc_length / segments as f32;
263
264 for i in 0..segments {
265 let angle1 = start_angle + i as f32 * angle_step;
266 let angle2 = start_angle + (i + 1) as f32 * angle_step;
267
268 let p1 = Pos2::new(
269 center.x + radius * angle1.cos(),
270 center.y + radius * angle1.sin(),
271 );
272 let p2 = Pos2::new(
273 center.x + radius * angle2.cos(),
274 center.y + radius * angle2.sin(),
275 );
276
277 ui.painter()
278 .line_segment([p1, p2], egui::Stroke::new(self.stroke_width, color));
279 }
280 }
281}