1use 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; #[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 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 pub fn width(mut self, width: impl Into<Length>) -> Self {
48 self.width = width.into();
49 self
50 }
51
52 pub fn girth(mut self, girth: impl Into<Length>) -> Self {
54 self.girth = girth.into();
55 self
56 }
57
58 pub fn style(mut self, style: impl Into<Theme::Style>) -> Self {
60 self.style = style.into();
61 self
62 }
63
64 pub fn cycle_duration(mut self, duration: Duration) -> Self {
66 self.cycle_duration = duration / 2;
67 self
68 }
69
70 pub fn period(mut self, duration: Duration) -> Self {
73 self.period = duration;
74 self
75 }
76
77 pub fn progress(mut self, progress: f32) -> Self {
79 self.progress = Some(progress.clamp(0.0, 1.0));
80 self
81 }
82
83 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 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 #[allow(clippy::too_many_lines)]
179 fn draw(
180 &self,
181 tree: &Tree,
182 renderer: &mut Renderer,
183 theme: &Theme,
184 _style: &renderer::Style,
185 layout: Layout<'_>,
186 _cursor: mouse::Cursor,
187 _viewport: &Rectangle,
188 ) {
189 let bounds = layout.bounds();
190 let custom_style = theme.appearance(&self.style, self.progress.is_some(), false);
191 let state = tree.state.downcast_ref::<State>();
192
193 let border_width = if custom_style.border_color.is_some() {
194 1.0
195 } else {
196 0.0
197 };
198 let border_color = custom_style.border_color.unwrap_or(custom_style.bar_color);
199 let radius = custom_style.border_radius;
200
201 let draw_quad = |renderer: &mut Renderer,
202 x: f32,
203 width: f32,
204 color: iced::Color,
205 border: iced::Border| {
206 renderer.fill_quad(
207 renderer::Quad {
208 bounds: Rectangle {
209 x: bounds.x + x * bounds.width,
210 y: bounds.y,
211 width: width * bounds.width,
212 height: bounds.height,
213 },
214 border,
215 snap: true,
216 ..renderer::Quad::default()
217 },
218 color,
219 );
220 };
221
222 if self.progress.is_some() {
224 let current_p = state.progress.current;
225 let len = self.markers.len();
226 let spacing = self.segment_spacing;
227 let radius_inner = radius.min(spacing);
228
229 let gap = if len != 0 {
230 spacing / bounds.width
231 } else {
232 0.0
233 };
234 let drawable = 1.0 - gap * len as f32;
235
236 for i in 0..=len {
237 let (seg_lo, r_left) = if i == 0 {
238 (0.0, radius)
239 } else {
240 (self.markers[i - 1], radius_inner)
241 };
242 let (seg_hi, r_right) = if i == len {
243 (1.0, radius)
244 } else {
245 (self.markers[i], radius_inner)
246 };
247 let x_start = seg_lo * drawable + i as f32 * gap;
248 let x_width = (seg_hi - seg_lo) * drawable;
249 let segment_radius = [r_left, r_right, r_right, r_left].into();
250
251 let border = iced::Border {
252 width: border_width,
253 color: border_color,
254 radius: segment_radius,
255 };
256
257 if current_p < seg_lo {
259 draw_quad(renderer, x_start, x_width, custom_style.track_color, border);
260 }
261 else if current_p > seg_hi {
263 draw_quad(renderer, x_start, x_width, custom_style.bar_color, border);
264 }
265 else {
267 let fill = ((current_p - seg_lo) / (seg_hi - seg_lo)).min(1.0);
268 draw_quad(renderer, x_start, x_width, custom_style.track_color, border);
269 renderer.with_layer(
270 Rectangle {
271 x: bounds.x + x_start * bounds.width,
272 y: bounds.y,
273 width: x_width * bounds.width * fill,
274 height: bounds.height,
275 },
276 |renderer| {
277 draw_quad(renderer, x_start, x_width, custom_style.bar_color, border);
278 },
279 );
280 }
281 }
282 }
283 else {
285 draw_quad(
287 renderer,
288 0.0,
289 1.0,
290 custom_style.track_color,
291 iced::Border {
292 width: border_width,
293 color: border_color,
294 radius: radius.into(),
295 },
296 );
297
298 let (bar_start, bar_end) =
300 state
301 .animation
302 .bar_positions(self.cycle_duration, MIN_LENGTH, WRAP_LENGTH);
303 let length = bar_end - bar_start;
304 let start = bar_start % 1.0;
305 let right_width = (1.0 - start).min(length);
306 let left_width = length - right_width;
307 let border = iced::Border {
308 radius: radius.into(),
309 ..iced::Border::default()
310 };
311
312 renderer.with_layer(
313 Rectangle {
314 x: bounds.x,
315 y: bounds.y,
316 width: left_width * bounds.width,
317 height: bounds.height,
318 },
319 |renderer| {
320 draw_quad(renderer, 0.0, 1.0, custom_style.bar_color, border);
321 },
322 );
323
324 renderer.with_layer(
325 Rectangle {
326 x: bounds.x + start * bounds.width,
327 y: bounds.y,
328 width: right_width * bounds.width,
329 height: bounds.height,
330 },
331 |renderer| {
332 draw_quad(renderer, 0.0, 1.0, custom_style.bar_color, border);
333 },
334 );
335 }
336 }
337}
338
339impl<'a, Message, Theme, Renderer> From<Linear<Theme>> for Element<'a, Message, Theme, Renderer>
340where
341 Message: Clone + 'a,
342 Theme: StyleSheet + 'a,
343 Renderer: iced::advanced::Renderer + 'a,
344{
345 fn from(linear: Linear<Theme>) -> Self {
346 Self::new(linear)
347 }
348}