Skip to main content

cosmic/widget/progress_bar/
linear.rs

1//! Show a linear 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::{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: Catalog,
17{
18    width: Length,
19    girth: Length,
20    class: Theme::Class,
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: Catalog<Class = style::Class>,
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            class: Theme::Class::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 class of this [`Linear`].
59    pub fn class(mut self, class: Theme::Class) -> Self {
60        self.class = class;
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    /// Sets the track color of this [`Linear`].
104    pub fn track_color(mut self, color: impl Into<iced::Color>) -> Self {
105        self.class = self.class.track_color(color);
106        self
107    }
108
109    /// Sets the bar color of this [`Linear`].
110    pub fn bar_color(mut self, color: impl Into<iced::Color>) -> Self {
111        self.class = self.class.bar_color(color);
112        self
113    }
114
115    /// Sets the border color of this [`Linear`].
116    pub fn border_color(mut self, color: impl Into<iced::Color>) -> Self {
117        self.class = self.class.border_color(color);
118        self
119    }
120
121    /// Sets the border radius of this [`Linear`].
122    pub fn border_radius(mut self, radius: f32) -> Self {
123        self.class = self.class.border_radius(radius);
124        self
125    }
126}
127
128impl<Theme> Default for Linear<Theme>
129where
130    Theme: Catalog<Class = style::Class>,
131{
132    fn default() -> Self {
133        Self::new()
134    }
135}
136
137#[derive(Default)]
138struct State {
139    animation: Animation,
140    progress: Progress,
141}
142
143impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Linear<Theme>
144where
145    Message: Clone,
146    Theme: Catalog,
147    Renderer: advanced::Renderer,
148{
149    fn tag(&self) -> tree::Tag {
150        tree::Tag::of::<State>()
151    }
152
153    fn state(&self) -> tree::State {
154        tree::State::new(State::default())
155    }
156
157    fn size(&self) -> Size<Length> {
158        Size {
159            width: self.width,
160            height: self.girth,
161        }
162    }
163
164    fn layout(
165        &mut self,
166        _tree: &mut Tree,
167        _renderer: &Renderer,
168        limits: &layout::Limits,
169    ) -> layout::Node {
170        layout::atomic(limits, self.width, self.girth)
171    }
172
173    fn update(
174        &mut self,
175        tree: &mut Tree,
176        event: &Event,
177        _layout: Layout<'_>,
178        _cursor: mouse::Cursor,
179        _renderer: &Renderer,
180        _clipboard: &mut dyn Clipboard,
181        shell: &mut Shell<'_, Message>,
182        _viewport: &Rectangle,
183    ) {
184        let state = tree.state.downcast_mut::<State>();
185        if let Event::Window(window::Event::RedrawRequested(now)) = event {
186            if let Some(target) = self.progress {
187                if state.progress.update(target, *now) {
188                    shell.request_redraw();
189                }
190            } else {
191                state.animation = state.animation.timed_transition(
192                    self.cycle_duration,
193                    self.period,
194                    WRAP_LENGTH,
195                    *now,
196                );
197                shell.request_redraw();
198            }
199        }
200    }
201
202    fn draw(
203        &self,
204        tree: &Tree,
205        renderer: &mut Renderer,
206        theme: &Theme,
207        _style: &renderer::Style,
208        layout: Layout<'_>,
209        _cursor: mouse::Cursor,
210        _viewport: &Rectangle,
211    ) {
212        let bounds = layout.bounds();
213        let custom_style = theme.style(&self.class, self.progress.is_some(), false);
214        let state = tree.state.downcast_ref::<State>();
215
216        let border_width = if custom_style.border_color.is_some() {
217            1.0
218        } else {
219            0.0
220        };
221        let border_color = custom_style.border_color.unwrap_or(custom_style.bar_color);
222        let radius = custom_style.border_radius;
223        let track_color = custom_style.track_color;
224        let bar_color = custom_style.bar_color;
225
226        let draw_quad = |renderer: &mut Renderer, rect: Rectangle, border: Border, color: Color| {
227            renderer.fill_quad(
228                renderer::Quad {
229                    bounds: rect,
230                    border,
231                    snap: true,
232                    ..renderer::Quad::default()
233                },
234                color,
235            );
236        };
237        let to_rect = |x: f32, width: f32| Rectangle {
238            x: bounds.x + x * bounds.width,
239            y: bounds.y,
240            width: width * bounds.width,
241            height: bounds.height,
242        };
243
244        // determinate progress bar
245        if self.progress.is_some() {
246            let current_p = state.progress.current;
247            let len = self.markers.len();
248            let spacing = self.segment_spacing;
249            let gap = spacing / bounds.width;
250            let drawable = 1.0 - gap * len as f32;
251            let radius_inner = radius.min(spacing);
252
253            for i in 0..=len {
254                let (seg_lo, r_left) = if i == 0 {
255                    (0.0, radius)
256                } else {
257                    (self.markers[i - 1], radius_inner)
258                };
259                let (seg_hi, r_right) = if i == len {
260                    (1.0, radius)
261                } else {
262                    (self.markers[i], radius_inner)
263                };
264                let x_start = seg_lo * drawable + i as f32 * gap;
265                let x_width = (seg_hi - seg_lo) * drawable;
266                let rect = to_rect(x_start, x_width);
267                let segment_radius = [r_left, r_right, r_right, r_left].into();
268
269                let border = Border {
270                    width: border_width,
271                    color: border_color,
272                    radius: segment_radius,
273                };
274
275                // empty segment
276                if current_p < seg_lo {
277                    draw_quad(renderer, rect, border, track_color);
278                }
279                // filled segment
280                else if current_p > seg_hi {
281                    draw_quad(renderer, rect, border, bar_color);
282                }
283                // partially filled segment
284                else {
285                    let fill = (current_p - seg_lo) / (seg_hi - seg_lo);
286                    draw_quad(renderer, rect, border, track_color);
287                    renderer.with_layer(to_rect(x_start, x_width * fill), |renderer| {
288                        draw_quad(renderer, rect, border, bar_color);
289                    });
290                }
291            }
292        }
293        // indeterminate progress bar
294        else {
295            // draw track
296            draw_quad(
297                renderer,
298                bounds,
299                Border {
300                    width: border_width,
301                    color: border_color,
302                    radius: radius.into(),
303                },
304                track_color,
305            );
306
307            // draw bar
308            let (bar_start, bar_end) =
309                state
310                    .animation
311                    .bar_positions(self.cycle_duration, MIN_LENGTH, WRAP_LENGTH);
312            let length = bar_end - bar_start;
313            let start = bar_start % 1.0;
314            let right_width = (1.0 - start).min(length);
315            let left_width = length - right_width;
316            let border = Border {
317                radius: radius.into(),
318                ..Border::default()
319            };
320
321            renderer.with_layer(to_rect(start, right_width), |renderer| {
322                draw_quad(renderer, bounds, border, bar_color);
323            });
324            renderer.with_layer(to_rect(0.0, left_width), |renderer| {
325                draw_quad(renderer, bounds, border, bar_color);
326            });
327        }
328    }
329}
330
331impl<'a, Message, Theme, Renderer> From<Linear<Theme>> for Element<'a, Message, Theme, Renderer>
332where
333    Message: Clone + 'a,
334    Theme: Catalog + 'a,
335    Renderer: iced::advanced::Renderer + 'a,
336{
337    fn from(linear: Linear<Theme>) -> Self {
338        Self::new(linear)
339    }
340}