cosmic/widget/progress_bar/
circular.rs

1//! Show a circular progress indicator.
2use 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    /// Creates a new [`Circular`] with the given content.
39    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    /// Sets the size of the [`Circular`].
51    pub fn size(mut self, size: f32) -> Self {
52        self.size = size;
53        self
54    }
55
56    /// Sets the bar height of the [`Circular`].
57    pub fn bar_height(mut self, bar_height: f32) -> Self {
58        self.bar_height = bar_height;
59        self
60    }
61
62    /// Sets the style variant of this [`Circular`].
63    pub fn style(mut self, style: <Theme as StyleSheet>::Style) -> Self {
64        self.style = style;
65        self
66    }
67
68    /// Sets the cycle duration of this [`Circular`].
69    pub fn cycle_duration(mut self, duration: Duration) -> Self {
70        self.cycle_duration = duration / 2;
71        self
72    }
73
74    /// Sets the base rotation duration of this [`Circular`]. This is the duration that a full
75    /// rotation would take if the cycle rotation were set to 0.0 (no expanding or contracting)
76    pub fn rotation_duration(mut self, duration: Duration) -> Self {
77        self.rotation_duration = duration;
78        self
79    }
80
81    /// Override the default behavior by providing a determinate progress value between `0.0` and `1.0`.
82    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                // outer border
310                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                // inner border
323                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                // bar
336                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                // get center of end of arc for rounded cap
357                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                // get center of start of arc for rounded cap
368                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                // get center of end of arc for rounded cap
413                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                // get center of start of arc for rounded cap
423                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}