Skip to main content

cosmic/widget/progress_bar/
linear.rs

1//! Show a linear 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::{Element, Event, Length, Pixels, Rectangle, Size, mouse, window};
7
8use std::time::Duration;
9
10const MIN_LENGTH: f32 = 0.15;
11const WRAP_LENGTH: f32 = 0.618; // avoids animation repetition
12
13#[must_use]
14pub struct Linear<Theme>
15where
16    Theme: StyleSheet,
17{
18    width: Length,
19    girth: Length,
20    style: Theme::Style,
21    cycle_duration: Duration,
22    period: Duration,
23    progress: Option<f32>,
24    markers: Vec<f32>,
25    segment_spacing: f32,
26}
27
28impl<Theme> Linear<Theme>
29where
30    Theme: StyleSheet,
31{
32    /// Creates a new [`Linear`] with the given content.
33    pub fn new() -> Self {
34        Linear {
35            width: Length::Fixed(100.0),
36            girth: Length::Fixed(4.0),
37            style: Theme::Style::default(),
38            cycle_duration: Duration::from_millis(1500),
39            period: Duration::from_secs(2),
40            progress: None,
41            markers: Vec::new(),
42            segment_spacing: 1.0,
43        }
44    }
45
46    /// Sets the width of the [`Linear`].
47    pub fn width(mut self, width: impl Into<Length>) -> Self {
48        self.width = width.into();
49        self
50    }
51
52    /// Sets the girth of the [`Linear`].
53    pub fn girth(mut self, girth: impl Into<Length>) -> Self {
54        self.girth = girth.into();
55        self
56    }
57
58    /// Sets the style variant of this [`Linear`].
59    pub fn style(mut self, style: impl Into<Theme::Style>) -> Self {
60        self.style = style.into();
61        self
62    }
63
64    /// Sets the cycle duration of this [`Linear`].
65    pub fn cycle_duration(mut self, duration: Duration) -> Self {
66        self.cycle_duration = duration / 2;
67        self
68    }
69
70    /// Sets the base period of this [`Linear`]. This is the duration that a full traversal
71    /// would take if the cycle duration were set to 0.0 (no expanding or contracting)
72    pub fn period(mut self, duration: Duration) -> Self {
73        self.period = duration;
74        self
75    }
76
77    /// Override the default behavior by providing a determinate progress value between `0.0` and `1.0`.
78    pub fn progress(mut self, progress: f32) -> Self {
79        self.progress = Some(progress.clamp(0.0, 1.0));
80        self
81    }
82
83    /// Sets the markers of a determinate progress bar, which divide the bar into segments.
84    /// Each marker is a value between `0.0` and `1.0` that defines the position of a visual gap.
85    pub fn markers(mut self, markers: impl Into<Vec<f32>>) -> Self {
86        let mut markers = markers.into();
87        for marker in &mut markers {
88            *marker = marker.clamp(0.0, 1.0);
89        }
90        markers.sort_by(f32::total_cmp);
91        markers.dedup();
92
93        self.markers = markers;
94        self
95    }
96
97    /// Sets the spacing between segments at each marker.
98    pub fn segment_spacing(mut self, spacing: impl Into<Pixels>) -> Self {
99        self.segment_spacing = spacing.into().0.max(1.0);
100        self
101    }
102}
103
104impl<Theme> Default for Linear<Theme>
105where
106    Theme: StyleSheet,
107{
108    fn default() -> Self {
109        Self::new()
110    }
111}
112
113#[derive(Default)]
114struct State {
115    animation: Animation,
116    progress: Progress,
117}
118
119impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Linear<Theme>
120where
121    Message: Clone,
122    Theme: StyleSheet,
123    Renderer: advanced::Renderer,
124{
125    fn tag(&self) -> tree::Tag {
126        tree::Tag::of::<State>()
127    }
128
129    fn state(&self) -> tree::State {
130        tree::State::new(State::default())
131    }
132
133    fn size(&self) -> Size<Length> {
134        Size {
135            width: self.width,
136            height: self.girth,
137        }
138    }
139
140    fn layout(
141        &mut self,
142        _tree: &mut Tree,
143        _renderer: &Renderer,
144        limits: &layout::Limits,
145    ) -> layout::Node {
146        layout::atomic(limits, self.width, self.girth)
147    }
148
149    fn update(
150        &mut self,
151        tree: &mut Tree,
152        event: &Event,
153        _layout: Layout<'_>,
154        _cursor: mouse::Cursor,
155        _renderer: &Renderer,
156        _clipboard: &mut dyn Clipboard,
157        shell: &mut Shell<'_, Message>,
158        _viewport: &Rectangle,
159    ) {
160        let state = tree.state.downcast_mut::<State>();
161        if let Event::Window(window::Event::RedrawRequested(now)) = event {
162            if let Some(target) = self.progress {
163                if state.progress.update(target, *now) {
164                    shell.request_redraw();
165                }
166            } else {
167                state.animation = state.animation.timed_transition(
168                    self.cycle_duration,
169                    self.period,
170                    WRAP_LENGTH,
171                    *now,
172                );
173                shell.request_redraw();
174            }
175        }
176    }
177
178    #[allow(clippy::too_many_lines)]
179    fn draw(
180        &self,
181        tree: &Tree,
182        renderer: &mut Renderer,
183        theme: &Theme,
184        _style: &renderer::Style,
185        layout: Layout<'_>,
186        _cursor: mouse::Cursor,
187        _viewport: &Rectangle,
188    ) {
189        let bounds = layout.bounds();
190        let custom_style = theme.appearance(&self.style, self.progress.is_some(), false);
191        let state = tree.state.downcast_ref::<State>();
192
193        let border_width = if custom_style.border_color.is_some() {
194            1.0
195        } else {
196            0.0
197        };
198        let border_color = custom_style.border_color.unwrap_or(custom_style.bar_color);
199        let radius = custom_style.border_radius;
200
201        let draw_quad = |renderer: &mut Renderer,
202                         x: f32,
203                         width: f32,
204                         color: iced::Color,
205                         border: iced::Border| {
206            renderer.fill_quad(
207                renderer::Quad {
208                    bounds: Rectangle {
209                        x: bounds.x + x * bounds.width,
210                        y: bounds.y,
211                        width: width * bounds.width,
212                        height: bounds.height,
213                    },
214                    border,
215                    snap: true,
216                    ..renderer::Quad::default()
217                },
218                color,
219            );
220        };
221
222        // determinate progress bar
223        if self.progress.is_some() {
224            let current_p = state.progress.current;
225            let len = self.markers.len();
226            let spacing = self.segment_spacing;
227            let radius_inner = radius.min(spacing);
228
229            let gap = if len != 0 {
230                spacing / bounds.width
231            } else {
232                0.0
233            };
234            let drawable = 1.0 - gap * len as f32;
235
236            for i in 0..=len {
237                let (seg_lo, r_left) = if i == 0 {
238                    (0.0, radius)
239                } else {
240                    (self.markers[i - 1], radius_inner)
241                };
242                let (seg_hi, r_right) = if i == len {
243                    (1.0, radius)
244                } else {
245                    (self.markers[i], radius_inner)
246                };
247                let x_start = seg_lo * drawable + i as f32 * gap;
248                let x_width = (seg_hi - seg_lo) * drawable;
249                let segment_radius = [r_left, r_right, r_right, r_left].into();
250
251                let border = iced::Border {
252                    width: border_width,
253                    color: border_color,
254                    radius: segment_radius,
255                };
256
257                // empty segment
258                if current_p < seg_lo {
259                    draw_quad(renderer, x_start, x_width, custom_style.track_color, border);
260                }
261                // filled segment
262                else if current_p > seg_hi {
263                    draw_quad(renderer, x_start, x_width, custom_style.bar_color, border);
264                }
265                // partially filled segment
266                else {
267                    let fill = ((current_p - seg_lo) / (seg_hi - seg_lo)).min(1.0);
268                    draw_quad(renderer, x_start, x_width, custom_style.track_color, border);
269                    renderer.with_layer(
270                        Rectangle {
271                            x: bounds.x + x_start * bounds.width,
272                            y: bounds.y,
273                            width: x_width * bounds.width * fill,
274                            height: bounds.height,
275                        },
276                        |renderer| {
277                            draw_quad(renderer, x_start, x_width, custom_style.bar_color, border);
278                        },
279                    );
280                }
281            }
282        }
283        // indeterminate progress bar
284        else {
285            // draw track
286            draw_quad(
287                renderer,
288                0.0,
289                1.0,
290                custom_style.track_color,
291                iced::Border {
292                    width: border_width,
293                    color: border_color,
294                    radius: radius.into(),
295                },
296            );
297
298            // draw bar
299            let (bar_start, bar_end) =
300                state
301                    .animation
302                    .bar_positions(self.cycle_duration, MIN_LENGTH, WRAP_LENGTH);
303            let length = bar_end - bar_start;
304            let start = bar_start % 1.0;
305            let right_width = (1.0 - start).min(length);
306            let left_width = length - right_width;
307            let border = iced::Border {
308                radius: radius.into(),
309                ..iced::Border::default()
310            };
311
312            renderer.with_layer(
313                Rectangle {
314                    x: bounds.x,
315                    y: bounds.y,
316                    width: left_width * bounds.width,
317                    height: bounds.height,
318                },
319                |renderer| {
320                    draw_quad(renderer, 0.0, 1.0, custom_style.bar_color, border);
321                },
322            );
323
324            renderer.with_layer(
325                Rectangle {
326                    x: bounds.x + start * bounds.width,
327                    y: bounds.y,
328                    width: right_width * bounds.width,
329                    height: bounds.height,
330                },
331                |renderer| {
332                    draw_quad(renderer, 0.0, 1.0, custom_style.bar_color, border);
333                },
334            );
335        }
336    }
337}
338
339impl<'a, Message, Theme, Renderer> From<Linear<Theme>> for Element<'a, Message, Theme, Renderer>
340where
341    Message: Clone + 'a,
342    Theme: StyleSheet + 'a,
343    Renderer: iced::advanced::Renderer + 'a,
344{
345    fn from(linear: Linear<Theme>) -> Self {
346        Self::new(linear)
347    }
348}