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    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
200        let mut draw_quad = |x: f32, width: f32, color: iced::Color, border: iced::Border| {
201            // don't draw if width is less than 0.1 pixels
202            if width * bounds.width > 0.1 {
203                renderer.fill_quad(
204                    renderer::Quad {
205                        bounds: Rectangle {
206                            x: bounds.x + x * bounds.width,
207                            y: bounds.y,
208                            width: width * bounds.width,
209                            height: bounds.height,
210                        },
211                        border,
212                        snap: true,
213                        ..renderer::Quad::default()
214                    },
215                    color,
216                );
217            }
218        };
219
220        if self.progress.is_some() {
221            let current_p = state.progress.current;
222            let len = self.markers.len();
223            let spacing = self.segment_spacing;
224            let radius_inner = radius.min(spacing);
225
226            let gap = if len != 0 {
227                spacing / bounds.width
228            } else {
229                0.0
230            };
231            let drawable = 1.0 - gap * len as f32;
232
233            for i in 0..=len {
234                let (seg_lo, r_left) = if i == 0 {
235                    (0.0, radius)
236                } else {
237                    (self.markers[i - 1], radius_inner)
238                };
239                let (seg_hi, r_right) = if i == len {
240                    (1.0, radius)
241                } else {
242                    (self.markers[i], radius_inner)
243                };
244                let x_start = seg_lo * drawable + i as f32 * gap;
245                let x_width = (seg_hi - seg_lo) * drawable;
246                let segment_radius = [r_left, r_right, r_right, r_left].into();
247
248                // draw track segment
249                draw_quad(
250                    x_start,
251                    x_width,
252                    custom_style.track_color,
253                    iced::Border {
254                        width: border_width,
255                        color: border_color,
256                        radius: segment_radius,
257                    },
258                );
259
260                // draw bar segment
261                if current_p > seg_lo {
262                    let fill = ((current_p - seg_lo) / (seg_hi - seg_lo)).min(1.0);
263                    draw_quad(
264                        x_start,
265                        x_width * fill,
266                        custom_style.bar_color,
267                        iced::Border {
268                            radius: segment_radius,
269                            ..iced::Border::default()
270                        },
271                    );
272                }
273            }
274        } else {
275            // draw track
276            draw_quad(
277                0.0,
278                1.0,
279                custom_style.track_color,
280                iced::Border {
281                    width: border_width,
282                    color: border_color,
283                    radius: radius.into(),
284                },
285            );
286
287            // draw bar
288            let (bar_start, bar_end) =
289                state
290                    .animation
291                    .bar_positions(self.cycle_duration, MIN_LENGTH, WRAP_LENGTH);
292            let length = bar_end - bar_start;
293            let start = bar_start % 1.0;
294            let right_width = (1.0 - start).min(length);
295            let left_width = length - right_width;
296            let border = iced::Border {
297                radius: radius.into(),
298                ..iced::Border::default()
299            };
300
301            draw_quad(start, right_width, custom_style.bar_color, border);
302            draw_quad(0.0, left_width, custom_style.bar_color, border);
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}