1use crate::ext::ArmasContextExt;
9use egui::{Color32, Pos2, Rect, Response, Ui, Vec2};
10use std::f32::consts::PI;
11
12const SPINNER_SIZE: f32 = 40.0;
13const SPINNER_BAR_COUNT: usize = 12;
14const SPINNER_BAR_WIDTH: f32 = 2.0;
15
16const SKELETON_CORNER_RADIUS: f32 = 6.0; const SKELETON_SHIMMER_WIDTH: f32 = 0.3;
18
19#[derive(Debug, Clone)]
34pub struct Spinner {
35 pub size: f32,
37 pub speed: f32,
39 color: Option<Color32>,
41 rotation: f32,
43 pub bar_count: usize,
45 pub bar_width: f32,
47}
48
49impl Default for Spinner {
50 fn default() -> Self {
51 Self::new()
52 }
53}
54
55impl Spinner {
56 #[must_use]
59 pub fn new() -> Self {
60 Self {
61 size: SPINNER_SIZE,
62 speed: 2.0 * PI,
63 color: None, rotation: 0.0,
65 bar_count: SPINNER_BAR_COUNT,
66 bar_width: SPINNER_BAR_WIDTH,
67 }
68 }
69
70 #[must_use]
72 pub const fn size(mut self, size: f32) -> Self {
73 self.size = size;
74 self
75 }
76
77 #[must_use]
79 pub const fn color(mut self, color: Color32) -> Self {
80 self.color = Some(color);
81 self
82 }
83
84 #[must_use]
86 pub const fn speed(mut self, speed: f32) -> Self {
87 self.speed = speed;
88 self
89 }
90
91 #[must_use]
93 pub fn bar_count(mut self, count: usize) -> Self {
94 self.bar_count = count.max(3);
95 self
96 }
97
98 pub fn show(&mut self, ui: &mut Ui) -> Response {
100 let theme = ui.ctx().armas_theme();
101 let (rect, response) = ui.allocate_exact_size(Vec2::splat(self.size), egui::Sense::hover());
102
103 let time = ui.input(|i| i.time) as f32;
105 self.rotation = (time * self.speed) % (2.0 * PI);
106
107 self.draw_spinner(ui, rect, &theme);
109
110 ui.ctx().request_repaint();
112
113 response
114 }
115
116 fn draw_spinner(&self, ui: &mut Ui, rect: Rect, theme: &crate::Theme) {
118 let painter = ui.painter();
119 let center = rect.center();
120 let radius = self.size / 2.0;
121 let bar_length = radius * 0.3;
122 let bar_start = radius * 0.5;
123
124 let base_color = self.color.unwrap_or_else(|| theme.primary());
126
127 for i in 0..self.bar_count {
128 let angle = (i as f32 / self.bar_count as f32) * 2.0 * PI + self.rotation;
129
130 let opacity_index = (self.bar_count - i) as f32 / self.bar_count as f32;
132 let alpha = (opacity_index * 255.0) as u8;
133
134 let color = Color32::from_rgba_unmultiplied(
135 base_color.r(),
136 base_color.g(),
137 base_color.b(),
138 alpha,
139 );
140
141 let start = Pos2::new(
143 center.x + angle.cos() * bar_start,
144 center.y + angle.sin() * bar_start,
145 );
146 let end = Pos2::new(
147 center.x + angle.cos() * (bar_start + bar_length),
148 center.y + angle.sin() * (bar_start + bar_length),
149 );
150
151 painter.line_segment([start, end], egui::Stroke::new(self.bar_width, color));
152 }
153 }
154}
155
156#[derive(Debug, Clone)]
171pub struct Skeleton {
172 pub width: f32,
174 pub height: f32,
176 base_color: Option<Color32>,
178 highlight_color: Option<Color32>,
180 shimmer_pos: f32,
182 pub speed: f32,
184 corner_radius: Option<f32>,
186 pub shimmer_width: f32,
188}
189
190impl Skeleton {
191 #[must_use]
194 pub const fn new(width: f32, height: f32) -> Self {
195 Self {
196 width,
197 height,
198 base_color: None, highlight_color: None, shimmer_pos: 0.0,
201 speed: 0.5,
202 corner_radius: Some(SKELETON_CORNER_RADIUS),
203 shimmer_width: SKELETON_SHIMMER_WIDTH,
204 }
205 }
206
207 #[must_use]
209 pub const fn base_color(mut self, color: Color32) -> Self {
210 self.base_color = Some(color);
211 self
212 }
213
214 #[must_use]
216 pub const fn highlight_color(mut self, color: Color32) -> Self {
217 self.highlight_color = Some(color);
218 self
219 }
220
221 #[must_use]
223 pub const fn speed(mut self, speed: f32) -> Self {
224 self.speed = speed;
225 self
226 }
227
228 #[must_use]
230 pub const fn corner_radius(mut self, radius: f32) -> Self {
231 self.corner_radius = Some(radius);
232 self
233 }
234
235 #[must_use]
237 pub const fn shimmer_width(mut self, width: f32) -> Self {
238 self.shimmer_width = width.clamp(0.1, 1.0);
239 self
240 }
241
242 pub fn show(&mut self, ui: &mut Ui) -> Response {
244 let theme = ui.ctx().armas_theme();
245 let (rect, response) =
246 ui.allocate_exact_size(Vec2::new(self.width, self.height), egui::Sense::hover());
247
248 let time = ui.input(|i| i.time) as f32;
250 self.shimmer_pos = (time * self.speed) % 1.0;
251
252 self.draw_skeleton(ui, rect, &theme);
254
255 ui.ctx().request_repaint();
257
258 response
259 }
260
261 fn draw_skeleton(&self, ui: &mut Ui, rect: Rect, theme: &crate::Theme) {
263 let painter = ui.painter();
264
265 let base_color = self.base_color.unwrap_or_else(|| theme.muted());
267 let highlight_color = self.highlight_color.unwrap_or_else(|| theme.card());
268 let corner_radius = self.corner_radius.unwrap_or(theme.spacing.xs);
269
270 painter.rect_filled(rect, corner_radius, base_color);
272
273 let shimmer_pixel_width = self.width * self.shimmer_width;
275 let shimmer_center = rect.min.x + self.shimmer_pos * (self.width + shimmer_pixel_width)
276 - shimmer_pixel_width / 2.0;
277
278 let steps = 20;
280 for i in 0..steps {
281 let offset_from_center = (i as f32 - steps as f32 / 2.0) / (steps as f32 / 2.0);
282 let x = shimmer_center + offset_from_center * shimmer_pixel_width / 2.0;
283
284 if x >= rect.min.x && x < rect.max.x {
286 let alpha_multiplier = 1.0 - offset_from_center.abs();
287 let alpha = (f32::from(highlight_color.a()) * alpha_multiplier) as u8;
288
289 let shimmer_color = Color32::from_rgba_unmultiplied(
290 highlight_color.r(),
291 highlight_color.g(),
292 highlight_color.b(),
293 alpha,
294 );
295
296 let step_width = shimmer_pixel_width / steps as f32;
297 let shimmer_rect = Rect::from_min_max(
298 Pos2::new(x, rect.min.y),
299 Pos2::new((x + step_width).min(rect.max.x), rect.max.y),
300 );
301
302 painter.rect_filled(shimmer_rect, corner_radius, shimmer_color);
303 }
304 }
305 }
306}
307
308impl Default for Skeleton {
309 fn default() -> Self {
310 Self::new(200.0, 20.0)
311 }
312}