Skip to main content

cosmic/widget/progress_bar/
circular.rs

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