1use super::style::StyleSheet;
3use crate::anim::smootherstep;
4use iced::advanced::layout;
5use iced::advanced::renderer;
6use iced::advanced::widget::tree::{self, Tree};
7use iced::advanced::{self, Clipboard, Layout, Shell, Widget};
8use iced::mouse;
9use iced::time::Instant;
10use iced::widget::canvas;
11use iced::window;
12use iced::{Element, Event, Length, Radians, Rectangle, Renderer, Size, Vector};
13
14use std::f32::consts::PI;
15use std::time::Duration;
16
17const MIN_ANGLE: Radians = Radians(PI / 8.0);
18const WRAP_ANGLE: Radians = Radians(2.0 * PI - PI / 4.0);
19const BASE_ROTATION_SPEED: u32 = u32::MAX / 80;
20
21#[must_use]
22pub struct Circular<Theme>
23where
24 Theme: StyleSheet,
25{
26 size: f32,
27 bar_height: f32,
28 style: <Theme as StyleSheet>::Style,
29 cycle_duration: Duration,
30 rotation_duration: Duration,
31 progress: Option<f32>,
32}
33
34impl<Theme> Circular<Theme>
35where
36 Theme: StyleSheet,
37{
38 pub fn new() -> Self {
40 Circular {
41 size: 40.0,
42 bar_height: 4.0,
43 style: <Theme as StyleSheet>::Style::default(),
44 cycle_duration: Duration::from_millis(1500),
45 rotation_duration: Duration::from_secs(2),
46 progress: None,
47 }
48 }
49
50 pub fn size(mut self, size: f32) -> Self {
52 self.size = size;
53 self
54 }
55
56 pub fn bar_height(mut self, bar_height: f32) -> Self {
58 self.bar_height = bar_height;
59 self
60 }
61
62 pub fn style(mut self, style: <Theme as StyleSheet>::Style) -> Self {
64 self.style = style;
65 self
66 }
67
68 pub fn cycle_duration(mut self, duration: Duration) -> Self {
70 self.cycle_duration = duration / 2;
71 self
72 }
73
74 pub fn rotation_duration(mut self, duration: Duration) -> Self {
77 self.rotation_duration = duration;
78 self
79 }
80
81 pub fn progress(mut self, progress: f32) -> Self {
83 self.progress = Some(progress.clamp(0.0, 1.0));
84 self
85 }
86}
87
88impl<Theme> Default for Circular<Theme>
89where
90 Theme: StyleSheet,
91{
92 fn default() -> Self {
93 Self::new()
94 }
95}
96
97#[derive(Clone, Copy)]
98enum Animation {
99 Expanding {
100 start: Instant,
101 progress: f32,
102 rotation: u32,
103 last: Instant,
104 },
105 Contracting {
106 start: Instant,
107 progress: f32,
108 rotation: u32,
109 last: Instant,
110 },
111}
112
113impl Default for Animation {
114 fn default() -> Self {
115 Self::Expanding {
116 start: Instant::now(),
117 progress: 0.0,
118 rotation: 0,
119 last: Instant::now(),
120 }
121 }
122}
123
124impl Animation {
125 fn next(&self, additional_rotation: u32, now: Instant) -> Self {
126 match self {
127 Self::Expanding { rotation, .. } => Self::Contracting {
128 start: now,
129 progress: 0.0,
130 rotation: rotation.wrapping_add(additional_rotation),
131 last: now,
132 },
133 Self::Contracting { rotation, .. } => Self::Expanding {
134 start: now,
135 progress: 0.0,
136 rotation: rotation.wrapping_add(BASE_ROTATION_SPEED.wrapping_add(
137 (f64::from(WRAP_ANGLE / (2.0 * Radians::PI)) * f64::from(u32::MAX)) as u32,
138 )),
139 last: now,
140 },
141 }
142 }
143
144 fn start(&self) -> Instant {
145 match self {
146 Self::Expanding { start, .. } | Self::Contracting { start, .. } => *start,
147 }
148 }
149
150 fn last(&self) -> Instant {
151 match self {
152 Self::Expanding { last, .. } | Self::Contracting { last, .. } => *last,
153 }
154 }
155
156 fn timed_transition(
157 &self,
158 cycle_duration: Duration,
159 rotation_duration: Duration,
160 now: Instant,
161 ) -> Self {
162 let elapsed = now.duration_since(self.start());
163 let additional_rotation = ((now - self.last()).as_secs_f32()
164 / rotation_duration.as_secs_f32()
165 * (u32::MAX) as f32) as u32;
166
167 match elapsed {
168 elapsed if elapsed > cycle_duration => self.next(additional_rotation, now),
169 _ => self.with_elapsed(cycle_duration, additional_rotation, elapsed, now),
170 }
171 }
172
173 fn with_elapsed(
174 &self,
175 cycle_duration: Duration,
176 additional_rotation: u32,
177 elapsed: Duration,
178 now: Instant,
179 ) -> Self {
180 let progress = elapsed.as_secs_f32() / cycle_duration.as_secs_f32();
181 match self {
182 Self::Expanding {
183 start, rotation, ..
184 } => Self::Expanding {
185 start: *start,
186 progress,
187 rotation: rotation.wrapping_add(additional_rotation),
188 last: now,
189 },
190 Self::Contracting {
191 start, rotation, ..
192 } => Self::Contracting {
193 start: *start,
194 progress,
195 rotation: rotation.wrapping_add(additional_rotation),
196 last: now,
197 },
198 }
199 }
200
201 fn rotation(&self) -> f32 {
202 match self {
203 Self::Expanding { rotation, .. } | Self::Contracting { rotation, .. } => {
204 *rotation as f32 / u32::MAX as f32
205 }
206 }
207 }
208}
209
210#[derive(Default)]
211struct State {
212 animation: Animation,
213 cache: canvas::Cache,
214 progress: Option<f32>,
215}
216
217impl<Message, Theme> Widget<Message, Theme, Renderer> for Circular<Theme>
218where
219 Message: Clone,
220 Theme: StyleSheet,
221{
222 fn tag(&self) -> tree::Tag {
223 tree::Tag::of::<State>()
224 }
225
226 fn state(&self) -> tree::State {
227 tree::State::new(State::default())
228 }
229
230 fn size(&self) -> Size<Length> {
231 Size {
232 width: Length::Fixed(self.size),
233 height: Length::Fixed(self.size),
234 }
235 }
236
237 fn layout(
238 &mut self,
239 _tree: &mut Tree,
240 _renderer: &Renderer,
241 limits: &layout::Limits,
242 ) -> layout::Node {
243 layout::atomic(limits, self.size, self.size)
244 }
245
246 fn update(
247 &mut self,
248 tree: &mut Tree,
249 event: &Event,
250 _layout: Layout<'_>,
251 _cursor: mouse::Cursor,
252 _renderer: &Renderer,
253 _clipboard: &mut dyn Clipboard,
254 shell: &mut Shell<'_, Message>,
255 _viewport: &Rectangle,
256 ) {
257 let state = tree.state.downcast_mut::<State>();
258 if self.progress.is_some() {
259 if !float_cmp::approx_eq!(
260 f32,
261 state.progress.unwrap_or_default(),
262 self.progress.unwrap_or_default()
263 ) {
264 state.progress = self.progress;
265 state.cache.clear();
266 }
267 return;
268 }
269 if let Event::Window(window::Event::RedrawRequested(now)) = event {
270 state.animation =
271 state
272 .animation
273 .timed_transition(self.cycle_duration, self.rotation_duration, *now);
274
275 state.cache.clear();
276 shell.request_redraw();
277 }
278 }
279
280 fn draw(
281 &self,
282 tree: &Tree,
283 renderer: &mut Renderer,
284 theme: &Theme,
285 _style: &renderer::Style,
286 layout: Layout<'_>,
287 _cursor: mouse::Cursor,
288 _viewport: &Rectangle,
289 ) {
290 use advanced::Renderer as _;
291
292 let state = tree.state.downcast_ref::<State>();
293 let bounds = layout.bounds();
294 let custom_style =
295 <Theme as StyleSheet>::appearance(theme, &self.style, self.progress.is_some(), true);
296
297 let geometry = state.cache.draw(renderer, bounds.size(), |frame| {
298 let track_radius = frame.width() / 2.0 - self.bar_height;
299 let track_path = canvas::Path::circle(frame.center(), track_radius);
300
301 frame.stroke(
302 &track_path,
303 canvas::Stroke::default()
304 .with_color(custom_style.track_color)
305 .with_width(self.bar_height),
306 );
307
308 if let Some(progress) = self.progress {
309 if let Some(border_color) = custom_style.border_color {
311 let border_path =
312 canvas::Path::circle(frame.center(), track_radius + self.bar_height / 2.0);
313
314 frame.stroke(
315 &border_path,
316 canvas::Stroke::default()
317 .with_color(border_color)
318 .with_width(1.0),
319 );
320 }
321
322 if let Some(border_color) = custom_style.border_color {
324 let border_path =
325 canvas::Path::circle(frame.center(), track_radius - self.bar_height / 2.0);
326
327 frame.stroke(
328 &border_path,
329 canvas::Stroke::default()
330 .with_color(border_color)
331 .with_width(1.0),
332 );
333 }
334
335 let mut builder = canvas::path::Builder::new();
337
338 builder.arc(canvas::path::Arc {
339 center: frame.center(),
340 radius: track_radius,
341 start_angle: Radians(-PI / 2.0),
342 end_angle: Radians(-PI / 2.0 + progress * 2.0 * PI),
343 });
344
345 let bar_path = builder.build();
346
347 frame.stroke(
348 &bar_path,
349 canvas::Stroke::default()
350 .with_color(custom_style.bar_color)
351 .with_width(self.bar_height),
352 );
353
354 let mut builder = canvas::path::Builder::new();
355
356 let end_angle = -PI / 2.0 + progress * 2.0 * PI;
358 let end_center =
359 frame.center() + Vector::new(end_angle.cos(), end_angle.sin()) * track_radius;
360 builder.arc(canvas::path::Arc {
361 center: end_center,
362 radius: self.bar_height / 2.0,
363 start_angle: Radians(end_angle),
364 end_angle: Radians(end_angle + PI),
365 });
366
367 let start_angle = -PI / 2.0;
369 let start_center = frame.center()
370 + Vector::new(start_angle.cos(), start_angle.sin()) * track_radius;
371 builder.arc(canvas::path::Arc {
372 center: start_center,
373 radius: self.bar_height / 2.0,
374 start_angle: Radians(start_angle - PI),
375 end_angle: Radians(start_angle),
376 });
377
378 let cap_path = builder.build();
379 frame.fill(&cap_path, custom_style.bar_color);
380 } else {
381 let mut builder = canvas::path::Builder::new();
382
383 let start = Radians(state.animation.rotation() * 2.0 * PI);
384 let (start_angle, end_angle) = match state.animation {
385 Animation::Expanding { progress, .. } => (
386 start,
387 start + MIN_ANGLE + WRAP_ANGLE * (smootherstep(progress)),
388 ),
389 Animation::Contracting { progress, .. } => (
390 start + WRAP_ANGLE * (smootherstep(progress)),
391 start + MIN_ANGLE + WRAP_ANGLE,
392 ),
393 };
394 builder.arc(canvas::path::Arc {
395 center: frame.center(),
396 radius: track_radius,
397 start_angle,
398 end_angle,
399 });
400
401 let bar_path = builder.build();
402
403 frame.stroke(
404 &bar_path,
405 canvas::Stroke::default()
406 .with_color(custom_style.bar_color)
407 .with_width(self.bar_height),
408 );
409
410 let mut builder = canvas::path::Builder::new();
411
412 let end_center = frame.center()
414 + Vector::new(end_angle.0.cos(), end_angle.0.sin()) * track_radius;
415 builder.arc(canvas::path::Arc {
416 center: end_center,
417 radius: self.bar_height / 2.0,
418 start_angle: Radians(end_angle.0),
419 end_angle: Radians(end_angle.0 + PI),
420 });
421
422 let start_center = frame.center()
424 + Vector::new(start_angle.0.cos(), start_angle.0.sin()) * track_radius;
425 builder.arc(canvas::path::Arc {
426 center: start_center,
427 radius: self.bar_height / 2.0,
428 start_angle: Radians(start_angle.0 - PI),
429 end_angle: Radians(start_angle.0),
430 });
431
432 let cap_path = builder.build();
433 frame.fill(&cap_path, custom_style.bar_color);
434 }
435 });
436
437 renderer.with_translation(Vector::new(bounds.x, bounds.y), |renderer| {
438 use iced::advanced::graphics::geometry::Renderer as _;
439
440 renderer.draw_geometry(geometry);
441 });
442 }
443}
444
445impl<'a, Message, Theme> From<Circular<Theme>> for Element<'a, Message, Theme, Renderer>
446where
447 Message: Clone + 'a,
448 Theme: StyleSheet + 'a,
449{
450 fn from(circular: Circular<Theme>) -> Self {
451 Self::new(circular)
452 }
453}