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::{Border, Color, 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    fn draw(
179        &self,
180        tree: &Tree,
181        renderer: &mut Renderer,
182        theme: &Theme,
183        _style: &renderer::Style,
184        layout: Layout<'_>,
185        _cursor: mouse::Cursor,
186        _viewport: &Rectangle,
187    ) {
188        let bounds = layout.bounds();
189        let custom_style = theme.appearance(&self.style, self.progress.is_some(), false);
190        let state = tree.state.downcast_ref::<State>();
191
192        let border_width = if custom_style.border_color.is_some() {
193            1.0
194        } else {
195            0.0
196        };
197        let border_color = custom_style.border_color.unwrap_or(custom_style.bar_color);
198        let radius = custom_style.border_radius;
199        let track_color = custom_style.track_color;
200        let bar_color = custom_style.bar_color;
201
202        let draw_quad = |renderer: &mut Renderer, rect: Rectangle, border: Border, color: Color| {
203            renderer.fill_quad(
204                renderer::Quad {
205                    bounds: rect,
206                    border,
207                    snap: true,
208                    ..renderer::Quad::default()
209                },
210                color,
211            );
212        };
213        let to_rect = |x: f32, width: f32| Rectangle {
214            x: bounds.x + x * bounds.width,
215            y: bounds.y,
216            width: width * bounds.width,
217            height: bounds.height,
218        };
219
220        // determinate progress bar
221        if self.progress.is_some() {
222            let current_p = state.progress.current;
223            let len = self.markers.len();
224            let spacing = self.segment_spacing;
225            let gap = spacing / bounds.width;
226            let drawable = 1.0 - gap * len as f32;
227            let radius_inner = radius.min(spacing);
228
229            for i in 0..=len {
230                let (seg_lo, r_left) = if i == 0 {
231                    (0.0, radius)
232                } else {
233                    (self.markers[i - 1], radius_inner)
234                };
235                let (seg_hi, r_right) = if i == len {
236                    (1.0, radius)
237                } else {
238                    (self.markers[i], radius_inner)
239                };
240                let x_start = seg_lo * drawable + i as f32 * gap;
241                let x_width = (seg_hi - seg_lo) * drawable;
242                let rect = to_rect(x_start, x_width);
243                let segment_radius = [r_left, r_right, r_right, r_left].into();
244
245                let border = Border {
246                    width: border_width,
247                    color: border_color,
248                    radius: segment_radius,
249                };
250
251                // empty segment
252                if current_p < seg_lo {
253                    draw_quad(renderer, rect, border, track_color);
254                }
255                // filled segment
256                else if current_p > seg_hi {
257                    draw_quad(renderer, rect, border, bar_color);
258                }
259                // partially filled segment
260                else {
261                    let fill = (current_p - seg_lo) / (seg_hi - seg_lo);
262                    draw_quad(renderer, rect, border, track_color);
263                    renderer.with_layer(to_rect(x_start, x_width * fill), |renderer| {
264                        draw_quad(renderer, rect, border, bar_color);
265                    });
266                }
267            }
268        }
269        // indeterminate progress bar
270        else {
271            // draw track
272            draw_quad(
273                renderer,
274                bounds,
275                Border {
276                    width: border_width,
277                    color: border_color,
278                    radius: radius.into(),
279                },
280                track_color,
281            );
282
283            // draw bar
284            let (bar_start, bar_end) =
285                state
286                    .animation
287                    .bar_positions(self.cycle_duration, MIN_LENGTH, WRAP_LENGTH);
288            let length = bar_end - bar_start;
289            let start = bar_start % 1.0;
290            let right_width = (1.0 - start).min(length);
291            let left_width = length - right_width;
292            let border = Border {
293                radius: radius.into(),
294                ..Border::default()
295            };
296
297            renderer.with_layer(to_rect(start, right_width), |renderer| {
298                draw_quad(renderer, bounds, border, bar_color);
299            });
300            renderer.with_layer(to_rect(0.0, left_width), |renderer| {
301                draw_quad(renderer, bounds, border, bar_color);
302            });
303        }
304    }
305}
306
307impl<'a, Message, Theme, Renderer> From<Linear<Theme>> for Element<'a, Message, Theme, Renderer>
308where
309    Message: Clone + 'a,
310    Theme: StyleSheet + 'a,
311    Renderer: iced::advanced::Renderer + 'a,
312{
313    fn from(linear: Linear<Theme>) -> Self {
314        Self::new(linear)
315    }
316}