1use std::f32::consts::TAU;
9
10use egui::{
11 epaint::{PathShape, PathStroke},
12 pos2, Color32, Response, Sense, Ui, Vec2, Widget, WidgetInfo, WidgetType,
13};
14
15use crate::theme::{with_alpha, Accent, Theme};
16
17#[derive(Debug, Clone, Copy)]
27#[must_use = "Add with `ui.add(...)`."]
28pub struct Spinner {
29 size: f32,
30 thickness: Option<f32>,
31 color: Option<Color32>,
32 accent: Option<Accent>,
33}
34
35impl Default for Spinner {
36 fn default() -> Self {
37 Self::new()
38 }
39}
40
41impl Spinner {
42 pub fn new() -> Self {
44 Self {
45 size: 18.0,
46 thickness: None,
47 color: None,
48 accent: None,
49 }
50 }
51
52 pub fn size(mut self, size: f32) -> Self {
54 self.size = size;
55 self
56 }
57
58 pub fn thickness(mut self, thickness: f32) -> Self {
60 self.thickness = Some(thickness);
61 self
62 }
63
64 pub fn color(mut self, color: Color32) -> Self {
66 self.color = Some(color);
67 self.accent = None;
68 self
69 }
70
71 pub fn accent(mut self, accent: Accent) -> Self {
74 self.accent = Some(accent);
75 self.color = None;
76 self
77 }
78}
79
80impl Widget for Spinner {
81 fn ui(self, ui: &mut Ui) -> Response {
82 let theme = Theme::current(ui.ctx());
83 let color = match (self.color, self.accent) {
84 (Some(c), _) => c,
85 (_, Some(a)) => theme.palette.accent_fill(a),
86 _ => theme.palette.sky,
87 };
88 let thickness = self.thickness.unwrap_or((self.size * 0.12).max(1.5));
89
90 let (rect, response) = ui.allocate_exact_size(Vec2::splat(self.size), Sense::hover());
91
92 if ui.is_rect_visible(rect) {
93 crate::request_repaint_at_rate(ui.ctx(), 30.0);
94 let painter = ui.painter();
95 let center = rect.center();
96 let radius = (self.size * 0.5) - thickness * 0.5 - 1.0;
97 let point_at = |a: f32| {
98 let (sin, cos) = a.sin_cos();
99 pos2(center.x + radius * cos, center.y + radius * sin)
100 };
101
102 let n_full: usize = 96;
106 let track_points: Vec<_> = (0..n_full)
107 .map(|i| point_at((i as f32 / n_full as f32) * TAU))
108 .collect();
109 painter.add(PathShape::closed_line(
110 track_points,
111 PathStroke::new(thickness, with_alpha(color, 40)),
112 ));
113
114 let t = ui.input(|i| i.time) as f32;
119 let phase = 0.5 - 0.5 * (t * 1.3).cos();
120 let sweep_min = TAU * 0.08;
121 let sweep_max = TAU * 0.75;
122 let sweep = sweep_min + (sweep_max - sweep_min) * phase;
123 let rotation = t * TAU * 0.85;
124
125 let n_points = 48;
126 let points: Vec<_> = (0..=n_points)
127 .map(|i| point_at(rotation + (i as f32 / n_points as f32) * sweep))
128 .collect();
129
130 painter.circle_filled(points[0], thickness * 0.5, color);
132 painter.circle_filled(points[n_points], thickness * 0.5, color);
133 painter.add(PathShape::line(points, PathStroke::new(thickness, color)));
134 }
135
136 response
137 .widget_info(|| WidgetInfo::labeled(WidgetType::ProgressIndicator, true, "loading"));
138 response
139 }
140}