cosmic/widget/progress_bar/
linear.rs1use 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 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 pub fn width(mut self, width: impl Into<Length>) -> Self {
46 self.width = width.into();
47 self
48 }
49
50 pub fn girth(mut self, girth: impl Into<Length>) -> Self {
52 self.girth = girth.into();
53 self
54 }
55
56 pub fn style(mut self, style: impl Into<Theme::Style>) -> Self {
58 self.style = style.into();
59 self
60 }
61
62 pub fn cycle_duration(mut self, duration: Duration) -> Self {
64 self.cycle_duration = duration / 2;
65 self
66 }
67
68 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}