1use 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#[allow(missing_debug_implementations)]
18pub struct Spinner {
19 width: Length,
21 height: Length,
23 rate: Duration,
25 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 #[must_use]
43 pub fn new() -> Self {
44 Self::default()
45 }
46
47 #[must_use]
49 pub fn width(mut self, width: impl Into<Length>) -> Self {
50 self.width = width.into();
51 self
52 }
53
54 #[must_use]
56 pub fn height(mut self, height: impl Into<Length>) -> Self {
57 self.height = height.into();
58 self
59 }
60
61 #[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}