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