egui/widgets/
spinner.rs

1use epaint::{emath::lerp, vec2, Color32, Pos2, Rect, Shape, Stroke};
2
3use crate::{Response, Sense, Ui, Widget, WidgetInfo, WidgetType};
4
5/// A spinner widget used to indicate loading.
6///
7/// See also: [`crate::ProgressBar`].
8#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
9#[derive(Default)]
10pub struct Spinner {
11    /// Uses the style's `interact_size` if `None`.
12    size: Option<f32>,
13    color: Option<Color32>,
14}
15
16impl Spinner {
17    /// Create a new spinner that uses the style's `interact_size` unless changed.
18    pub fn new() -> Self {
19        Self::default()
20    }
21
22    /// Sets the spinner's size. The size sets both the height and width, as the spinner is always
23    /// square. If the size isn't set explicitly, the active style's `interact_size` is used.
24    #[inline]
25    pub fn size(mut self, size: f32) -> Self {
26        self.size = Some(size);
27        self
28    }
29
30    /// Sets the spinner's color.
31    #[inline]
32    pub fn color(mut self, color: impl Into<Color32>) -> Self {
33        self.color = Some(color.into());
34        self
35    }
36
37    /// Paint the spinner in the given rectangle.
38    pub fn paint_at(&self, ui: &Ui, rect: Rect) {
39        if ui.is_rect_visible(rect) {
40            ui.ctx().request_repaint(); // because it is animated
41
42            let color = self
43                .color
44                .unwrap_or_else(|| ui.visuals().strong_text_color());
45            let radius = (rect.height() / 2.0) - 2.0;
46            let n_points = 20;
47            let time = ui.input(|i| i.time);
48            let start_angle = time * std::f64::consts::TAU;
49            let end_angle = start_angle + 240f64.to_radians() * time.sin();
50            let points: Vec<Pos2> = (0..n_points)
51                .map(|i| {
52                    let angle = lerp(start_angle..=end_angle, i as f64 / n_points as f64);
53                    let (sin, cos) = angle.sin_cos();
54                    rect.center() + radius * vec2(cos as f32, sin as f32)
55                })
56                .collect();
57            ui.painter()
58                .add(Shape::line(points, Stroke::new(3.0, color)));
59        }
60    }
61}
62
63impl Widget for Spinner {
64    fn ui(self, ui: &mut Ui) -> Response {
65        let size = self
66            .size
67            .unwrap_or_else(|| ui.style().spacing.interact_size.y);
68        let (rect, response) = ui.allocate_exact_size(vec2(size, size), Sense::hover());
69        response.widget_info(|| WidgetInfo::new(WidgetType::ProgressIndicator));
70        self.paint_at(ui, rect);
71
72        response
73    }
74}