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: 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: 40.0,
36            bar_height: 4.0,
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    pub fn bar_height(mut self, bar_height: f32) -> Self {
52        self.bar_height = bar_height;
53        self
54    }
55
56    /// Sets the style variant of this [`Circular`].
57    pub fn style(mut self, style: Theme::Style) -> Self {
58        self.style = style;
59        self
60    }
61
62    /// Sets the cycle duration of this [`Circular`].
63    pub fn cycle_duration(mut self, duration: Duration) -> Self {
64        self.cycle_duration = duration / 2;
65        self
66    }
67
68    /// Sets the base period of this [`Circular`]. This is the duration that a full rotation
69    /// would take if the cycle duration were set to 0.0 (no expanding or contracting)
70    pub fn period(mut self, duration: Duration) -> Self {
71        self.period = duration;
72        self
73    }
74
75    /// Override the default behavior by providing a determinate progress value between `0.0` and `1.0`.
76    pub fn progress(mut self, progress: f32) -> Self {
77        self.progress = Some(progress.clamp(0.0, 1.0));
78        self
79    }
80}
81
82impl<Theme> Default for Circular<Theme>
83where
84    Theme: StyleSheet,
85{
86    fn default() -> Self {
87        Self::new()
88    }
89}
90
91#[derive(Default)]
92struct State {
93    animation: Animation,
94    cache: canvas::Cache,
95    progress: Progress,
96}
97
98impl<Message, Theme> Widget<Message, Theme, Renderer> for Circular<Theme>
99where
100    Message: Clone,
101    Theme: StyleSheet,
102{
103    fn tag(&self) -> tree::Tag {
104        tree::Tag::of::<State>()
105    }
106
107    fn state(&self) -> tree::State {
108        tree::State::new(State::default())
109    }
110
111    fn size(&self) -> Size<Length> {
112        Size {
113            width: Length::Fixed(self.size),
114            height: Length::Fixed(self.size),
115        }
116    }
117
118    fn layout(
119        &mut self,
120        _tree: &mut Tree,
121        _renderer: &Renderer,
122        limits: &layout::Limits,
123    ) -> layout::Node {
124        layout::atomic(limits, self.size, self.size)
125    }
126
127    fn update(
128        &mut self,
129        tree: &mut Tree,
130        event: &Event,
131        _layout: Layout<'_>,
132        _cursor: mouse::Cursor,
133        _renderer: &Renderer,
134        _clipboard: &mut dyn Clipboard,
135        shell: &mut Shell<'_, Message>,
136        _viewport: &Rectangle,
137    ) {
138        let state = tree.state.downcast_mut::<State>();
139        if let Event::Window(window::Event::RedrawRequested(now)) = event {
140            if let Some(target) = self.progress {
141                if state.progress.update(target, *now) {
142                    state.cache.clear();
143                    shell.request_redraw();
144                }
145            } else {
146                state.animation = state.animation.timed_transition(
147                    self.cycle_duration,
148                    self.period,
149                    MAX_WRAP,
150                    *now,
151                );
152                state.cache.clear();
153                shell.request_redraw();
154            }
155        }
156    }
157
158    fn draw(
159        &self,
160        tree: &Tree,
161        renderer: &mut Renderer,
162        theme: &Theme,
163        _style: &renderer::Style,
164        layout: Layout<'_>,
165        _cursor: mouse::Cursor,
166        _viewport: &Rectangle,
167    ) {
168        use advanced::Renderer as _;
169
170        let state = tree.state.downcast_ref::<State>();
171        let bounds = layout.bounds();
172        let custom_style = Theme::appearance(theme, &self.style, self.progress.is_some(), true);
173
174        let geometry = state.cache.draw(renderer, bounds.size(), |frame| {
175            let track_radius = frame.width() / 2.0 - self.bar_height;
176            if track_radius <= 0.0 {
177                return;
178            }
179            let track_path = canvas::Path::circle(frame.center(), track_radius);
180
181            frame.stroke(
182                &track_path,
183                canvas::Stroke::default()
184                    .with_color(custom_style.track_color)
185                    .with_width(self.bar_height),
186            );
187
188            // Converts a track fraction to an angle in radians, with 0 being top of circle
189            let to_angle = |t: f32| t * 2.0 * PI - PI / 2.0;
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(to_angle(start)),
196                    end_angle: Radians(to_angle(end)),
197                });
198                frame.stroke(
199                    &builder.build(),
200                    canvas::Stroke::default()
201                        .with_color(custom_style.bar_color)
202                        .with_width(self.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                    for radius_offset in [self.bar_height / 2.0, -(self.bar_height / 2.0)] {
210                        let border_path =
211                            canvas::Path::circle(frame.center(), track_radius + radius_offset);
212                        frame.stroke(
213                            &border_path,
214                            canvas::Stroke::default()
215                                .with_color(border_color)
216                                .with_width(1.0),
217                        );
218                    }
219                }
220                draw_bar(frame, 0.0, state.progress.current);
221            } else {
222                // f32::EPSILON prevents flicker when wrap angle is 0.0
223                let (start, end) =
224                    state
225                        .animation
226                        .bar_positions(self.cycle_duration, f32::EPSILON, MAX_WRAP);
227                draw_bar(frame, start, end);
228            }
229        });
230
231        renderer.with_translation(Vector::new(bounds.x, bounds.y), |renderer| {
232            use iced::advanced::graphics::geometry::Renderer as _;
233
234            renderer.draw_geometry(geometry);
235        });
236    }
237}
238
239impl<'a, Message, Theme> From<Circular<Theme>> for Element<'a, Message, Theme, Renderer>
240where
241    Message: Clone + 'a,
242    Theme: StyleSheet + 'a,
243{
244    fn from(circular: Circular<Theme>) -> Self {
245        Self::new(circular)
246    }
247}