cosmic/widget/progress_bar/
circular.rs

1//! Show a circular progress indicator.
2use super::animation::{Animation, Progress};
3use super::style::StyleSheet;
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::widget::canvas;
10use iced::window;
11use iced::{Element, Event, Length, Radians, Rectangle, Renderer, Size, Vector};
12
13use std::f32::consts::PI;
14use std::time::Duration;
15
16const MIN_ANGLE: Radians = Radians(PI / 8.0);
17
18#[must_use]
19pub struct Circular<Theme>
20where
21    Theme: StyleSheet,
22{
23    size: f32,
24    bar_height: f32,
25    style: Theme::Style,
26    cycle_duration: Duration,
27    period: Duration,
28    progress: Option<f32>,
29}
30
31impl<Theme> Circular<Theme>
32where
33    Theme: StyleSheet,
34{
35    /// Creates a new [`Circular`] with the given content.
36    pub fn new() -> Self {
37        Circular {
38            size: 40.0,
39            bar_height: 4.0,
40            style: Theme::Style::default(),
41            cycle_duration: Duration::from_millis(1500),
42            period: Duration::from_secs(2),
43            progress: None,
44        }
45    }
46
47    /// Sets the size of the [`Circular`].
48    pub fn size(mut self, size: f32) -> Self {
49        self.size = size;
50        self
51    }
52
53    /// Sets the bar height of the [`Circular`].
54    pub fn bar_height(mut self, bar_height: f32) -> Self {
55        self.bar_height = bar_height;
56        self
57    }
58
59    /// Sets the style variant of this [`Circular`].
60    pub fn style(mut self, style: Theme::Style) -> Self {
61        self.style = style;
62        self
63    }
64
65    /// Sets the cycle duration of this [`Circular`].
66    pub fn cycle_duration(mut self, duration: Duration) -> Self {
67        self.cycle_duration = duration / 2;
68        self
69    }
70
71    /// Sets the base period of this [`Circular`]. This is the duration that a full rotation
72    /// would take if the cycle duration were set to 0.0 (no expanding or contracting)
73    pub fn period(mut self, duration: Duration) -> Self {
74        self.period = duration;
75        self
76    }
77
78    /// Override the default behavior by providing a determinate progress value between `0.0` and `1.0`.
79    pub fn progress(mut self, progress: f32) -> Self {
80        self.progress = Some(progress.clamp(0.0, 1.0));
81        self
82    }
83
84    fn min_wrap(&self, track_radius: f32) -> (f32, f32) {
85        let cap_angle = self.bar_height / track_radius;
86        let gap = MIN_ANGLE.0.max(cap_angle);
87        ((gap - cap_angle) / (2.0 * PI), 1.0 - gap / PI)
88    }
89}
90
91impl<Theme> Default for Circular<Theme>
92where
93    Theme: StyleSheet,
94{
95    fn default() -> Self {
96        Self::new()
97    }
98}
99
100#[derive(Default)]
101struct State {
102    animation: Animation,
103    cache: canvas::Cache,
104    progress: Progress,
105}
106
107impl<Message, Theme> Widget<Message, Theme, Renderer> for Circular<Theme>
108where
109    Message: Clone,
110    Theme: StyleSheet,
111{
112    fn tag(&self) -> tree::Tag {
113        tree::Tag::of::<State>()
114    }
115
116    fn state(&self) -> tree::State {
117        tree::State::new(State::default())
118    }
119
120    fn size(&self) -> Size<Length> {
121        Size {
122            width: Length::Fixed(self.size),
123            height: Length::Fixed(self.size),
124        }
125    }
126
127    fn layout(
128        &mut self,
129        _tree: &mut Tree,
130        _renderer: &Renderer,
131        limits: &layout::Limits,
132    ) -> layout::Node {
133        layout::atomic(limits, self.size, self.size)
134    }
135
136    fn update(
137        &mut self,
138        tree: &mut Tree,
139        event: &Event,
140        _layout: Layout<'_>,
141        _cursor: mouse::Cursor,
142        _renderer: &Renderer,
143        _clipboard: &mut dyn Clipboard,
144        shell: &mut Shell<'_, Message>,
145        _viewport: &Rectangle,
146    ) {
147        let state = tree.state.downcast_mut::<State>();
148        if let Event::Window(window::Event::RedrawRequested(now)) = event {
149            if let Some(target) = self.progress {
150                if state.progress.update(target, *now) {
151                    state.cache.clear();
152                    shell.request_redraw();
153                }
154            } else {
155                let (_, wrap) = self.min_wrap(self.size / 2.0 - self.bar_height);
156                state.animation =
157                    state
158                        .animation
159                        .timed_transition(self.cycle_duration, self.period, wrap, *now);
160                state.cache.clear();
161                shell.request_redraw();
162            }
163        }
164    }
165
166    fn draw(
167        &self,
168        tree: &Tree,
169        renderer: &mut Renderer,
170        theme: &Theme,
171        _style: &renderer::Style,
172        layout: Layout<'_>,
173        _cursor: mouse::Cursor,
174        _viewport: &Rectangle,
175    ) {
176        use advanced::Renderer as _;
177
178        let state = tree.state.downcast_ref::<State>();
179        let bounds = layout.bounds();
180        let custom_style = Theme::appearance(theme, &self.style, self.progress.is_some(), true);
181
182        let geometry = state.cache.draw(renderer, bounds.size(), |frame| {
183            let track_radius = frame.width() / 2.0 - self.bar_height;
184            let track_path = canvas::Path::circle(frame.center(), track_radius);
185
186            frame.stroke(
187                &track_path,
188                canvas::Stroke::default()
189                    .with_color(custom_style.track_color)
190                    .with_width(self.bar_height),
191            );
192
193            // Converts a track fraction to an angle in radians, with 0 being top of circle
194            let to_angle = |t: f32| t * 2.0 * PI - PI / 2.0;
195
196            let draw_cap = |frame: &mut canvas::Frame, t: f32, flip: bool| {
197                let angle = to_angle(t);
198                let center = frame.center() + Vector::new(angle.cos(), angle.sin()) * track_radius;
199                let (start_angle, end_angle) = if flip {
200                    (angle - PI, angle)
201                } else {
202                    (angle, angle + PI)
203                };
204                let mut builder = canvas::path::Builder::new();
205                builder.arc(canvas::path::Arc {
206                    center,
207                    radius: self.bar_height / 2.0,
208                    start_angle: Radians(start_angle),
209                    end_angle: Radians(end_angle),
210                });
211                frame.fill(&builder.build(), custom_style.bar_color);
212            };
213
214            let draw_bar = |frame: &mut canvas::Frame, start: f32, end: f32| {
215                let mut builder = canvas::path::Builder::new();
216                builder.arc(canvas::path::Arc {
217                    center: frame.center(),
218                    radius: track_radius,
219                    start_angle: Radians(to_angle(start)),
220                    end_angle: Radians(to_angle(end)),
221                });
222                frame.stroke(
223                    &builder.build(),
224                    canvas::Stroke::default()
225                        .with_color(custom_style.bar_color)
226                        .with_width(self.bar_height),
227                );
228                draw_cap(frame, end, false);
229                draw_cap(frame, start, true);
230            };
231
232            if self.progress.is_some() {
233                if let Some(border_color) = custom_style.border_color {
234                    for radius_offset in [self.bar_height / 2.0, -(self.bar_height / 2.0)] {
235                        let border_path =
236                            canvas::Path::circle(frame.center(), track_radius + radius_offset);
237                        frame.stroke(
238                            &border_path,
239                            canvas::Stroke::default()
240                                .with_color(border_color)
241                                .with_width(1.0),
242                        );
243                    }
244                }
245                draw_bar(frame, 0.0, state.progress.current);
246            } else {
247                let (min, wrap) = self.min_wrap(track_radius);
248                let (start, end) = state
249                    .animation
250                    .bar_positions(self.cycle_duration, min, wrap);
251                draw_bar(frame, start, end);
252            }
253        });
254
255        renderer.with_translation(Vector::new(bounds.x, bounds.y), |renderer| {
256            use iced::advanced::graphics::geometry::Renderer as _;
257
258            renderer.draw_geometry(geometry);
259        });
260    }
261}
262
263impl<'a, Message, Theme> From<Circular<Theme>> for Element<'a, Message, Theme, Renderer>
264where
265    Message: Clone + 'a,
266    Theme: StyleSheet + 'a,
267{
268    fn from(circular: Circular<Theme>) -> Self {
269        Self::new(circular)
270    }
271}