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