iced_aw/widget/
spinner.rs

1//! A spinner to suggest something is loading.
2use iced_core::{
3    Border, Clipboard, Color, Element, Event, Layout, Length, Rectangle, Shell, Size, Vector,
4    Widget,
5    layout::{Limits, Node},
6    mouse::Cursor,
7    renderer,
8    time::{Duration, Instant},
9    widget::{
10        Tree,
11        tree::{State, Tag},
12    },
13    window,
14};
15
16/// A spinner widget, a circle spinning around the center of the widget.
17#[allow(missing_debug_implementations)]
18pub struct Spinner {
19    /// The width of the [`Spinner`].
20    width: Length,
21    /// The height of the [`Spinner`].
22    height: Length,
23    /// The rate of the [`Spinner`].
24    rate: Duration,
25    /// The radius of the spinning circle.
26    circle_radius: f32,
27}
28
29impl Default for Spinner {
30    fn default() -> Self {
31        Self {
32            width: Length::Fixed(20.0),
33            height: Length::Fixed(20.0),
34            rate: Duration::from_secs_f32(1.0),
35            circle_radius: 2.0,
36        }
37    }
38}
39
40impl Spinner {
41    /// Creates a new [`Spinner`] widget.
42    #[must_use]
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Sets the width of the [`Spinner`].
48    #[must_use]
49    pub fn width(mut self, width: impl Into<Length>) -> Self {
50        self.width = width.into();
51        self
52    }
53
54    /// Sets the height of the [`Spinner`].
55    #[must_use]
56    pub fn height(mut self, height: impl Into<Length>) -> Self {
57        self.height = height.into();
58        self
59    }
60
61    /// Sets the circle radius of the spinning circle.
62    #[must_use]
63    pub fn circle_radius(mut self, radius: f32) -> Self {
64        self.circle_radius = radius;
65        self
66    }
67}
68
69struct SpinnerState {
70    last_update: Instant,
71    t: f32,
72}
73
74fn is_visible(bounds: &Rectangle) -> bool {
75    bounds.width > 0.0 && bounds.height > 0.0
76}
77
78fn fill_circle(
79    renderer: &mut impl renderer::Renderer,
80    position: Vector,
81    radius: f32,
82    color: Color,
83) {
84    if radius > 0. {
85        renderer.fill_quad(
86            renderer::Quad {
87                bounds: Rectangle {
88                    x: position.x,
89                    y: position.y,
90                    width: radius * 2.0,
91                    height: radius * 2.0,
92                },
93                border: Border {
94                    radius: radius.into(),
95                    width: 0.0,
96                    color: Color::TRANSPARENT,
97                },
98                ..Default::default()
99            },
100            color,
101        );
102    }
103}
104
105impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Spinner
106where
107    Renderer: renderer::Renderer,
108{
109    fn size(&self) -> Size<Length> {
110        Size::new(self.width, self.height)
111    }
112
113    fn layout(&mut self, _tree: &mut Tree, _renderer: &Renderer, limits: &Limits) -> Node {
114        Node::new(limits.width(self.width).height(self.height).resolve(
115            self.width,
116            self.height,
117            Size::new(f32::INFINITY, f32::INFINITY),
118        ))
119    }
120
121    fn draw(
122        &self,
123        state: &Tree,
124        renderer: &mut Renderer,
125        _theme: &Theme,
126        style: &renderer::Style,
127        layout: Layout<'_>,
128        _cursor: Cursor,
129        _viewport: &Rectangle,
130    ) {
131        let bounds = layout.bounds();
132
133        if !is_visible(&bounds) {
134            return;
135        }
136
137        let size = if bounds.width < bounds.height {
138            bounds.width
139        } else {
140            bounds.height
141        } / 2.0;
142        let state = state.state.downcast_ref::<SpinnerState>();
143        let center = bounds.center();
144        let distance_from_center = size - self.circle_radius;
145        let (y, x) = (state.t * std::f32::consts::PI * 2.0).sin_cos();
146        let position = Vector::new(
147            center.x + x * distance_from_center - self.circle_radius,
148            center.y + y * distance_from_center - self.circle_radius,
149        );
150
151        fill_circle(renderer, position, self.circle_radius, style.text_color);
152    }
153
154    fn tag(&self) -> Tag {
155        Tag::of::<SpinnerState>()
156    }
157
158    fn state(&self) -> State {
159        State::new(SpinnerState {
160            last_update: Instant::now(),
161            t: 0.0,
162        })
163    }
164
165    fn update(
166        &mut self,
167        state: &mut Tree,
168        event: &Event,
169        layout: Layout<'_>,
170        _cursor: Cursor,
171        _renderer: &Renderer,
172        _clipboard: &mut dyn Clipboard,
173        shell: &mut Shell<'_, Message>,
174        _viewport: &Rectangle,
175    ) {
176        const FRAMES_PER_SECOND: u64 = 60;
177
178        let bounds = layout.bounds();
179
180        if let Event::Window(window::Event::RedrawRequested(now)) = event
181            && is_visible(&bounds)
182        {
183            let state = state.state.downcast_mut::<SpinnerState>();
184            let duration = (*now - state.last_update).as_secs_f32();
185            let increment = if self.rate == Duration::ZERO {
186                0.0
187            } else {
188                duration * 1.0 / self.rate.as_secs_f32()
189            };
190
191            state.t += increment;
192
193            if state.t > 1.0 {
194                state.t -= 1.0;
195            }
196
197            shell.request_redraw_at(window::RedrawRequest::At(
198                *now + Duration::from_millis(1000 / FRAMES_PER_SECOND),
199            ));
200            state.last_update = *now;
201        }
202    }
203}
204
205impl<'a, Message, Theme, Renderer> From<Spinner> for Element<'a, Message, Theme, Renderer>
206where
207    Renderer: renderer::Renderer + 'a,
208{
209    fn from(spinner: Spinner) -> Self {
210        Self::new(spinner)
211    }
212}