Skip to main content

cosmic/widget/progress_bar/
circular.rs

1//! Show a circular progress indicator.
2use super::animation::{Animation, Progress};
3use super::style::StyleSheet;
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: StyleSheet,
19{
20    size: f32,
21    bar_height: Option<f32>,
22    style: Theme::Style,
23    cycle_duration: Duration,
24    period: Duration,
25    progress: Option<f32>,
26}
27
28impl<Theme> Circular<Theme>
29where
30    Theme: StyleSheet,
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            style: Theme::Style::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 variant of this [`Circular`].
58    pub fn style(mut self, style: Theme::Style) -> Self {
59        self.style = style;
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
83impl<Theme> Default for Circular<Theme>
84where
85    Theme: StyleSheet,
86{
87    fn default() -> Self {
88        Self::new()
89    }
90}
91
92#[derive(Default)]
93struct State {
94    animation: Animation,
95    cache: canvas::Cache,
96    progress: Progress,
97}
98
99impl<Message, Theme> Widget<Message, Theme, Renderer> for Circular<Theme>
100where
101    Message: Clone,
102    Theme: StyleSheet,
103{
104    fn tag(&self) -> tree::Tag {
105        tree::Tag::of::<State>()
106    }
107
108    fn state(&self) -> tree::State {
109        tree::State::new(State::default())
110    }
111
112    fn size(&self) -> Size<Length> {
113        Size {
114            width: Length::Fixed(self.size),
115            height: Length::Fixed(self.size),
116        }
117    }
118
119    fn layout(
120        &mut self,
121        _tree: &mut Tree,
122        _renderer: &Renderer,
123        limits: &layout::Limits,
124    ) -> layout::Node {
125        layout::atomic(limits, self.size, self.size)
126    }
127
128    fn update(
129        &mut self,
130        tree: &mut Tree,
131        event: &Event,
132        _layout: Layout<'_>,
133        _cursor: mouse::Cursor,
134        _renderer: &Renderer,
135        _clipboard: &mut dyn Clipboard,
136        shell: &mut Shell<'_, Message>,
137        _viewport: &Rectangle,
138    ) {
139        let state = tree.state.downcast_mut::<State>();
140        if let Event::Window(window::Event::RedrawRequested(now)) = event {
141            if let Some(target) = self.progress {
142                if state.progress.update(target, *now) {
143                    state.cache.clear();
144                    shell.request_redraw();
145                }
146            } else {
147                state.animation = state.animation.timed_transition(
148                    self.cycle_duration,
149                    self.period,
150                    MAX_WRAP,
151                    *now,
152                );
153                state.cache.clear();
154                shell.request_redraw();
155            }
156        }
157    }
158
159    fn draw(
160        &self,
161        tree: &Tree,
162        renderer: &mut Renderer,
163        theme: &Theme,
164        _style: &renderer::Style,
165        layout: Layout<'_>,
166        _cursor: mouse::Cursor,
167        _viewport: &Rectangle,
168    ) {
169        use advanced::Renderer as _;
170
171        let state = tree.state.downcast_ref::<State>();
172        let bounds = layout.bounds();
173        let custom_style = Theme::appearance(theme, &self.style, self.progress.is_some(), true);
174
175        let geometry = state.cache.draw(renderer, bounds.size(), |frame| {
176            let bar_height = self.bar_height.unwrap_or((frame.width() / 12.0).max(2.0));
177            let track_radius = (frame.width() - bar_height) / 2.0;
178            if track_radius <= 0.0 {
179                return;
180            }
181
182            let track_path = canvas::Path::circle(frame.center(), track_radius);
183            frame.stroke(
184                &track_path,
185                canvas::Stroke::default()
186                    .with_color(custom_style.track_color)
187                    .with_width(bar_height),
188            );
189
190            let draw_bar = |frame: &mut canvas::Frame, start: f32, end: f32| {
191                let mut builder = canvas::path::Builder::new();
192                builder.arc(canvas::path::Arc {
193                    center: frame.center(),
194                    radius: track_radius,
195                    start_angle: Radians(2.0 * PI * start - PI / 2.0),
196                    end_angle: Radians(2.0 * PI * end - PI / 2.0),
197                });
198                frame.stroke(
199                    &builder.build(),
200                    canvas::Stroke::default()
201                        .with_color(custom_style.bar_color)
202                        .with_width(bar_height)
203                        .with_line_cap(canvas::LineCap::Round),
204                );
205            };
206
207            if self.progress.is_some() {
208                if let Some(border_color) = custom_style.border_color {
209                    // - 0.5 ensures the border is inside the track
210                    for radius_offset in [bar_height / 2.0 - 0.5, -(bar_height / 2.0 - 0.5)] {
211                        let border_path =
212                            canvas::Path::circle(frame.center(), track_radius + radius_offset);
213                        frame.stroke(
214                            &border_path,
215                            canvas::Stroke::default()
216                                .with_color(border_color)
217                                .with_width(1.0),
218                        );
219                    }
220                }
221                draw_bar(frame, 0.0, state.progress.current);
222            } else {
223                // f32::EPSILON prevents flicker when wrap angle is 0.0
224                let (start, end) =
225                    state
226                        .animation
227                        .bar_positions(self.cycle_duration, f32::EPSILON, MAX_WRAP);
228                draw_bar(frame, start, end);
229            }
230        });
231
232        renderer.with_translation(Vector::new(bounds.x, bounds.y), |renderer| {
233            use iced::advanced::graphics::geometry::Renderer as _;
234
235            renderer.draw_geometry(geometry);
236        });
237    }
238}
239
240impl<'a, Message, Theme> From<Circular<Theme>> for Element<'a, Message, Theme, Renderer>
241where
242    Message: Clone + 'a,
243    Theme: StyleSheet + 'a,
244{
245    fn from(circular: Circular<Theme>) -> Self {
246        Self::new(circular)
247    }
248}