cosmic/widget/progress_bar/
linear.rs

1//! Show a linear progress indicator.
2use iced::advanced::layout;
3use iced::advanced::renderer::{self, Quad};
4use iced::advanced::widget::tree::{self, Tree};
5use iced::advanced::{self, Clipboard, Layout, Shell, Widget};
6use iced::mouse;
7use iced::time::Instant;
8use iced::window;
9use iced::{Background, Element, Event, Length, Rectangle, Size};
10
11use crate::anim::smootherstep;
12
13use super::style::StyleSheet;
14
15use std::time::Duration;
16
17#[must_use]
18pub struct Linear<Theme>
19where
20    Theme: StyleSheet,
21{
22    width: Length,
23    girth: Length,
24    style: Theme::Style,
25    cycle_duration: Duration,
26    progress: Option<f32>,
27}
28
29impl<Theme> Linear<Theme>
30where
31    Theme: StyleSheet,
32{
33    /// Creates a new [`Linear`] with the given content.
34    pub fn new() -> Self {
35        Linear {
36            width: Length::Fixed(100.0),
37            girth: Length::Fixed(4.0),
38            style: Theme::Style::default(),
39            cycle_duration: Duration::from_millis(1500),
40            progress: None,
41        }
42    }
43
44    /// Sets the width of the [`Linear`].
45    pub fn width(mut self, width: impl Into<Length>) -> Self {
46        self.width = width.into();
47        self
48    }
49
50    /// Sets the girth of the [`Linear`].
51    pub fn girth(mut self, girth: impl Into<Length>) -> Self {
52        self.girth = girth.into();
53        self
54    }
55
56    /// Sets the style variant of this [`Linear`].
57    pub fn style(mut self, style: impl Into<Theme::Style>) -> Self {
58        self.style = style.into();
59        self
60    }
61
62    /// Sets the cycle duration of this [`Linear`].
63    pub fn cycle_duration(mut self, duration: Duration) -> Self {
64        self.cycle_duration = duration / 2;
65        self
66    }
67
68    /// Override the default behavior by providing a determinate progress value between `0.0` and `1.0`.
69    pub fn progress(mut self, progress: f32) -> Self {
70        self.progress = Some(progress.clamp(0.0, 1.0));
71        self
72    }
73}
74
75impl<Theme> Default for Linear<Theme>
76where
77    Theme: StyleSheet,
78{
79    fn default() -> Self {
80        Self::new()
81    }
82}
83
84#[derive(Clone, Copy)]
85enum State {
86    Expanding { start: Instant, progress: f32 },
87    Contracting { start: Instant, progress: f32 },
88}
89
90impl Default for State {
91    fn default() -> Self {
92        Self::Expanding {
93            start: Instant::now(),
94            progress: 0.0,
95        }
96    }
97}
98
99impl State {
100    fn next(&self, now: Instant) -> Self {
101        match self {
102            Self::Expanding { .. } => Self::Contracting {
103                start: now,
104                progress: 0.0,
105            },
106            Self::Contracting { .. } => Self::Expanding {
107                start: now,
108                progress: 0.0,
109            },
110        }
111    }
112
113    fn start(&self) -> Instant {
114        match self {
115            Self::Expanding { start, .. } | Self::Contracting { start, .. } => *start,
116        }
117    }
118
119    fn timed_transition(&self, cycle_duration: Duration, now: Instant) -> Self {
120        let elapsed = now.duration_since(self.start());
121
122        match elapsed {
123            elapsed if elapsed > cycle_duration => self.next(now),
124            _ => self.with_elapsed(cycle_duration, elapsed),
125        }
126    }
127
128    fn with_elapsed(&self, cycle_duration: Duration, elapsed: Duration) -> Self {
129        let progress = elapsed.as_secs_f32() / cycle_duration.as_secs_f32();
130        match self {
131            Self::Expanding { start, .. } => Self::Expanding {
132                start: *start,
133                progress,
134            },
135            Self::Contracting { start, .. } => Self::Contracting {
136                start: *start,
137                progress,
138            },
139        }
140    }
141}
142
143impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Linear<Theme>
144where
145    Message: Clone,
146    Theme: StyleSheet,
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        if self.progress.is_some() {
185            return;
186        }
187
188        let state = tree.state.downcast_mut::<State>();
189
190        if let Event::Window(window::Event::RedrawRequested(now)) = event {
191            *state = state.timed_transition(self.cycle_duration, *now);
192
193            shell.request_redraw();
194        }
195    }
196
197    fn draw(
198        &self,
199        tree: &Tree,
200        renderer: &mut Renderer,
201        theme: &Theme,
202        _style: &renderer::Style,
203        layout: Layout<'_>,
204        _cursor: mouse::Cursor,
205        _viewport: &Rectangle,
206    ) {
207        let bounds = layout.bounds();
208        let custom_style = theme.appearance(&self.style, self.progress.is_some(), false);
209        let state = tree.state.downcast_ref::<State>();
210
211        renderer.fill_quad(
212            renderer::Quad {
213                bounds: Rectangle {
214                    x: bounds.x,
215                    y: bounds.y,
216                    width: bounds.width,
217                    height: bounds.height,
218                },
219                border: iced::Border {
220                    width: if custom_style.border_color.is_some() {
221                        1.0
222                    } else {
223                        0.0
224                    },
225                    color: custom_style.border_color.unwrap_or(custom_style.bar_color),
226                    radius: custom_style.border_radius.into(),
227                },
228                snap: true,
229                ..renderer::Quad::default()
230            },
231            Background::Color(custom_style.track_color),
232        );
233
234        if let Some(progress) = self.progress {
235            renderer.fill_quad(
236                renderer::Quad {
237                    bounds: Rectangle {
238                        x: bounds.x,
239                        y: bounds.y,
240                        width: progress * bounds.width,
241                        height: bounds.height,
242                    },
243                    border: iced::Border {
244                        width: 0.,
245                        color: iced::Color::TRANSPARENT,
246                        radius: custom_style.border_radius.into(),
247                    },
248                    snap: true,
249                    ..renderer::Quad::default()
250                },
251                Background::Color(custom_style.bar_color),
252            );
253        } else {
254            match state {
255                State::Expanding { progress, .. } => renderer.fill_quad(
256                    renderer::Quad {
257                        bounds: Rectangle {
258                            x: bounds.x,
259                            y: bounds.y,
260                            width: smootherstep(*progress) * bounds.width,
261                            height: bounds.height,
262                        },
263                        border: iced::Border {
264                            width: 0.,
265                            color: iced::Color::TRANSPARENT,
266                            radius: custom_style.border_radius.into(),
267                        },
268                        snap: true,
269                        ..renderer::Quad::default()
270                    },
271                    Background::Color(custom_style.bar_color),
272                ),
273
274                State::Contracting { progress, .. } => renderer.fill_quad(
275                    Quad {
276                        bounds: Rectangle {
277                            x: bounds.x + smootherstep(*progress) * bounds.width,
278                            y: bounds.y,
279                            width: (1.0 - smootherstep(*progress)) * bounds.width,
280                            height: bounds.height,
281                        },
282                        border: iced::Border {
283                            width: 0.,
284                            color: iced::Color::TRANSPARENT,
285                            radius: custom_style.border_radius.into(),
286                        },
287                        snap: true,
288                        ..renderer::Quad::default()
289                    },
290                    Background::Color(custom_style.bar_color),
291                ),
292            }
293        }
294    }
295}
296
297impl<'a, Message, Theme, Renderer> From<Linear<Theme>> for Element<'a, Message, Theme, Renderer>
298where
299    Message: Clone + 'a,
300    Theme: StyleSheet + 'a,
301    Renderer: iced::advanced::Renderer + 'a,
302{
303    fn from(linear: Linear<Theme>) -> Self {
304        Self::new(linear)
305    }
306}