Skip to main content

flashkraft_gui/ui/components/
animated_progress.rs

1//! Animated Progress Bar with Canvas Effects
2//!
3//! This module provides a custom animated progress bar using Iced's canvas
4//! for smooth, animated gradient and shimmer effects during flash operations.
5
6use iced::widget::canvas::{self, Cache, Frame, Geometry, Path, Stroke};
7use iced::{Color, Element, Length, Point, Rectangle, Renderer, Size, Theme};
8
9/// Animated progress bar with shimmer effect
10#[derive(Debug)]
11pub struct AnimatedProgress {
12    /// Progress value (0.0 to 1.0)
13    progress: f32,
14    /// Animation time for shimmer effect
15    animation_time: f32,
16    /// Cache for the progress bar rendering
17    cache: Cache,
18    /// Theme for color palette
19    theme: Theme,
20    /// Optional color override — when `Some`, replaces the theme primary color.
21    /// Used to render the verification bar in green instead of the accent color.
22    color_override: Option<Color>,
23}
24
25impl AnimatedProgress {
26    /// Create a new animated progress bar
27    pub fn new() -> Self {
28        Self {
29            progress: 0.0,
30            animation_time: 0.0,
31            cache: Cache::new(),
32            theme: Theme::Dark,
33            color_override: None,
34        }
35    }
36
37    /// Create a new animated progress bar with a fixed color override.
38    pub fn new_with_color(color: Color) -> Self {
39        Self {
40            progress: 0.0,
41            animation_time: 0.0,
42            cache: Cache::new(),
43            theme: Theme::Dark,
44            color_override: Some(color),
45        }
46    }
47
48    /// Set or clear the color override at runtime.
49    pub fn set_color_override(&mut self, color: Option<Color>) {
50        self.color_override = color;
51        self.cache.clear();
52    }
53
54    /// Update the progress value
55    pub fn set_progress(&mut self, progress: f32) {
56        let new_progress = progress.clamp(0.0, 1.0);
57        if (self.progress - new_progress).abs() > 0.001 {
58            self.progress = new_progress;
59            self.cache.clear();
60        }
61    }
62
63    /// Update animation time based on transfer speed
64    ///
65    /// # Arguments
66    /// * `speed_mb_s` - Current transfer speed in MB/s to scale animation speed
67    pub fn tick(&mut self, speed_mb_s: f32) {
68        // Scale animation speed based on transfer rate
69        // - At 1 MB/s: 0.15x speed (slow)
70        // - At 20 MB/s: 0.5x speed (baseline)
71        // - At 100+ MB/s: 1.2x speed (fast, capped)
72        let speed_multiplier = (speed_mb_s / 20.0).clamp(0.15, 1.2);
73        self.animation_time += 0.016 * speed_multiplier;
74        if self.animation_time > 1000.0 {
75            self.animation_time = 0.0;
76        }
77        self.cache.clear();
78    }
79
80    /// Set the theme for color rendering
81    pub fn set_theme(&mut self, theme: Theme) {
82        self.theme = theme;
83        self.cache.clear();
84    }
85
86    /// Return a clone of this bar's color override (if any).
87    pub fn color_override(&self) -> Option<Color> {
88        self.color_override
89    }
90
91    /// Create the widget view
92    pub fn view<'a, Message: 'a>(&'a self) -> Element<'a, Message, Theme, Renderer> {
93        iced::widget::canvas(self)
94            .width(Length::Fill)
95            .height(Length::Fixed(20.0))
96            .into()
97    }
98}
99
100impl Default for AnimatedProgress {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106impl<Message> canvas::Program<Message, Theme, Renderer> for AnimatedProgress {
107    type State = ();
108
109    fn draw(
110        &self,
111        _state: &Self::State,
112        renderer: &Renderer,
113        _theme: &Theme,
114        bounds: Rectangle,
115        _cursor: iced::mouse::Cursor,
116    ) -> Vec<Geometry<Renderer>> {
117        let geometry = self.cache.draw(renderer, bounds.size(), |frame| {
118            draw_animated_progress(
119                frame,
120                bounds.size(),
121                self.progress,
122                self.animation_time,
123                &self.theme,
124                self.color_override,
125            );
126        });
127
128        vec![geometry]
129    }
130}
131
132/// Draw the animated progress bar with effects
133fn draw_animated_progress(
134    frame: &mut Frame,
135    size: Size,
136    progress: f32,
137    time: f32,
138    theme: &Theme,
139    color_override: Option<Color>,
140) {
141    let width = size.width;
142    let height = size.height;
143
144    // Background (dark gray)
145    let background = Path::rectangle(Point::ORIGIN, size);
146    frame.fill(&background, Color::from_rgb(0.15, 0.15, 0.15));
147
148    if progress > 0.0 {
149        let progress_width = width * progress;
150
151        // Main progress bar with gradient effect
152        draw_gradient_progress(frame, progress_width, height, time, theme, color_override);
153
154        // Add shimmer/shine effect
155        draw_shimmer_effect(frame, progress_width, height, time);
156
157        // Add subtle pulse effect at the edge
158        if progress < 1.0 {
159            draw_edge_pulse(frame, progress_width, height, time);
160        }
161    }
162
163    // Border
164    let border = Path::rectangle(Point::ORIGIN, size);
165    frame.stroke(
166        &border,
167        Stroke::default()
168            .with_color(Color::from_rgb(0.3, 0.3, 0.3))
169            .with_width(1.0),
170    );
171}
172
173/// Draw gradient progress fill
174fn draw_gradient_progress(
175    frame: &mut Frame,
176    width: f32,
177    height: f32,
178    time: f32,
179    theme: &Theme,
180    color_override: Option<Color>,
181) {
182    // Use override color when provided (e.g. green for verification),
183    // otherwise fall back to the theme primary.
184    let primary = color_override.unwrap_or_else(|| theme.palette().primary);
185
186    // Create color variants from the base color
187    let color_start = Color::from_rgb(primary.r, primary.g, primary.b);
188    let color_end = Color::from_rgb(
189        (primary.r * 0.7 + 0.3).min(1.0),
190        (primary.g * 0.9 + 0.1).min(1.0),
191        (primary.b * 0.95 + 0.05).min(1.0),
192    );
193    // Create a multi-segment gradient effect
194    let segments = 20;
195    let segment_width = width / segments as f32;
196
197    for i in 0..segments {
198        let x = i as f32 * segment_width;
199        let progress_ratio = i as f32 / segments as f32;
200
201        // Animated color shifting
202        let hue_shift = (time * 0.12 + progress_ratio * 2.0).sin() * 0.06;
203
204        // Theme-based gradient with animation
205        let base_color = interpolate_color(color_start, color_end, progress_ratio);
206
207        let animated_color = Color {
208            r: (base_color.r + hue_shift).clamp(0.0, 1.0),
209            g: (base_color.g + hue_shift * 0.5).clamp(0.0, 1.0),
210            b: (base_color.b - hue_shift * 0.3).clamp(0.0, 1.0),
211            a: 1.0,
212        };
213
214        let segment = Path::rectangle(Point::new(x, 0.0), Size::new(segment_width + 1.0, height));
215        frame.fill(&segment, animated_color);
216    }
217}
218
219/// Draw shimmer/shine effect
220fn draw_shimmer_effect(frame: &mut Frame, width: f32, height: f32, time: f32) {
221    // Moving shine effect
222    let shine_position = (time * 0.18).fract();
223    let shine_x = width * shine_position;
224    let shine_width = width * 0.15;
225
226    // Create shimmer with gradient
227    let shimmer_segments = 10;
228    for i in 0..shimmer_segments {
229        let offset = i as f32 / shimmer_segments as f32;
230        let x = shine_x + (offset - 0.5) * shine_width;
231
232        if x >= 0.0 && x <= width {
233            // Gaussian-like falloff
234            let distance_from_center = ((offset - 0.5) * 2.0).abs();
235            let alpha = (1.0 - distance_from_center.powf(2.0)) * 0.3;
236
237            let shimmer = Path::rectangle(
238                Point::new(x, 0.0),
239                Size::new(shine_width / shimmer_segments as f32, height),
240            );
241
242            frame.fill(
243                &shimmer,
244                Color {
245                    r: 1.0,
246                    g: 1.0,
247                    b: 1.0,
248                    a: alpha,
249                },
250            );
251        }
252    }
253}
254
255/// Draw pulsing effect at the leading edge
256fn draw_edge_pulse(frame: &mut Frame, width: f32, height: f32, time: f32) {
257    let pulse = (time * 1.8).sin() * 0.5 + 0.5;
258    let pulse_width = 4.0 + pulse * 2.0;
259
260    // Glowing edge
261    let edge = Path::rectangle(
262        Point::new(width - pulse_width / 2.0, 0.0),
263        Size::new(pulse_width, height),
264    );
265
266    frame.fill(
267        &edge,
268        Color {
269            r: 1.0,
270            g: 1.0,
271            b: 1.0,
272            a: 0.3 + pulse * 0.3,
273        },
274    );
275}
276
277/// Interpolate between two colors
278fn interpolate_color(start: Color, end: Color, t: f32) -> Color {
279    Color {
280        r: start.r + (end.r - start.r) * t,
281        g: start.g + (end.g - start.g) * t,
282        b: start.b + (end.b - start.b) * t,
283        a: start.a + (end.a - start.a) * t,
284    }
285}
286
287// ---------------------------------------------------------------------------
288// Tests
289// ---------------------------------------------------------------------------
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    // ── new() defaults ───────────────────────────────────────────────────────
296
297    #[test]
298    fn test_new_defaults() {
299        let bar = AnimatedProgress::new();
300        assert_eq!(bar.progress, 0.0);
301        assert_eq!(bar.animation_time, 0.0);
302        assert!(bar.color_override.is_none());
303    }
304
305    #[test]
306    fn test_default_trait_matches_new() {
307        let a = AnimatedProgress::new();
308        let b = AnimatedProgress::default();
309        assert_eq!(a.progress, b.progress);
310        assert_eq!(a.animation_time, b.animation_time);
311        assert_eq!(a.color_override.is_none(), b.color_override.is_none());
312    }
313
314    // ── new_with_color ───────────────────────────────────────────────────────
315
316    #[test]
317    fn test_new_with_color() {
318        let green = Color::from_rgb(0.0, 1.0, 0.0);
319        let bar = AnimatedProgress::new_with_color(green);
320        let c = bar.color_override().unwrap();
321        assert_eq!(c.r, 0.0);
322        assert_eq!(c.g, 1.0);
323        assert_eq!(c.b, 0.0);
324    }
325
326    // ── set_progress clamping ────────────────────────────────────────────────
327
328    #[test]
329    fn test_set_progress_normal_value() {
330        let mut bar = AnimatedProgress::new();
331        bar.set_progress(0.5);
332        assert!((bar.progress - 0.5).abs() < 1e-6);
333    }
334
335    #[test]
336    fn test_set_progress_clamps_above_one() {
337        let mut bar = AnimatedProgress::new();
338        bar.set_progress(1.5);
339        assert!((bar.progress - 1.0).abs() < 1e-6);
340    }
341
342    #[test]
343    fn test_set_progress_clamps_below_zero() {
344        let mut bar = AnimatedProgress::new();
345        bar.set_progress(-0.3);
346        assert!((bar.progress - 0.0).abs() < 1e-6);
347    }
348
349    #[test]
350    fn test_set_progress_skips_tiny_change() {
351        let mut bar = AnimatedProgress::new();
352        bar.set_progress(0.5);
353        // A delta of 0.0001 is below the 0.001 threshold — progress should stay at 0.5.
354        bar.set_progress(0.5001);
355        assert!((bar.progress - 0.5).abs() < 1e-6);
356    }
357
358    // ── tick ─────────────────────────────────────────────────────────────────
359
360    #[test]
361    fn test_tick_increments_animation_time() {
362        let mut bar = AnimatedProgress::new();
363        assert_eq!(bar.animation_time, 0.0);
364        bar.tick(20.0); // speed_multiplier = 1.0 → +0.016
365        assert!(bar.animation_time > 0.0);
366    }
367
368    #[test]
369    fn test_tick_wraps_at_1000() {
370        let mut bar = AnimatedProgress::new();
371        bar.animation_time = 999.999;
372        bar.tick(20.0); // should push past 1000 and wrap to 0
373        assert!(bar.animation_time < 1.0, "animation_time should wrap to 0");
374    }
375
376    // ── interpolate_color ────────────────────────────────────────────────────
377
378    #[test]
379    fn test_interpolate_color_extremes() {
380        let black = Color::from_rgba(0.0, 0.0, 0.0, 1.0);
381        let white = Color::from_rgba(1.0, 1.0, 1.0, 1.0);
382
383        // t=0 → start
384        let c0 = interpolate_color(black, white, 0.0);
385        assert!((c0.r - 0.0).abs() < 1e-6);
386        assert!((c0.g - 0.0).abs() < 1e-6);
387        assert!((c0.b - 0.0).abs() < 1e-6);
388
389        // t=1 → end
390        let c1 = interpolate_color(black, white, 1.0);
391        assert!((c1.r - 1.0).abs() < 1e-6);
392        assert!((c1.g - 1.0).abs() < 1e-6);
393        assert!((c1.b - 1.0).abs() < 1e-6);
394
395        // t=0.5 → midpoint
396        let c05 = interpolate_color(black, white, 0.5);
397        assert!((c05.r - 0.5).abs() < 1e-6);
398        assert!((c05.g - 0.5).abs() < 1e-6);
399        assert!((c05.b - 0.5).abs() < 1e-6);
400    }
401
402    // ── set_color_override ──────────────────────────────────────────────────
403
404    #[test]
405    fn test_set_color_override_and_clear() {
406        let mut bar = AnimatedProgress::new();
407        assert!(bar.color_override().is_none());
408
409        let red = Color::from_rgb(1.0, 0.0, 0.0);
410        bar.set_color_override(Some(red));
411        assert!(bar.color_override().is_some());
412
413        bar.set_color_override(None);
414        assert!(bar.color_override().is_none());
415    }
416
417    // ── set_progress edge cases ──────────────────────────────────────────
418
419    #[test]
420    fn test_set_progress_clamps_negative_half() {
421        let mut bar = AnimatedProgress::new();
422        bar.set_progress(-0.5);
423        assert!((bar.progress - 0.0).abs() < 1e-6);
424    }
425
426    #[test]
427    fn test_set_progress_same_value_no_panic() {
428        let mut bar = AnimatedProgress::new();
429        bar.set_progress(0.5);
430        // Setting the exact same value again should be a no-op (delta < 0.001).
431        bar.set_progress(0.5);
432        assert!((bar.progress - 0.5).abs() < 1e-6);
433    }
434
435    // ── tick speed clamping ──────────────────────────────────────────────
436
437    #[test]
438    fn test_tick_with_zero_speed_uses_minimum_multiplier() {
439        let mut bar = AnimatedProgress::new();
440        bar.tick(0.0);
441        // speed_multiplier = (0.0 / 20.0).clamp(0.15, 1.2) = 0.15
442        // animation_time += 0.016 * 0.15 = 0.0024
443        let expected = 0.016 * 0.15;
444        assert!(
445            (bar.animation_time - expected).abs() < 1e-6,
446            "expected {expected}, got {}",
447            bar.animation_time,
448        );
449    }
450
451    #[test]
452    fn test_tick_with_high_speed_uses_maximum_multiplier() {
453        let mut bar = AnimatedProgress::new();
454        bar.tick(200.0);
455        // speed_multiplier = (200.0 / 20.0).clamp(0.15, 1.2) = 1.2
456        // animation_time += 0.016 * 1.2 = 0.0192
457        let expected = 0.016 * 1.2;
458        assert!(
459            (bar.animation_time - expected).abs() < 1e-6,
460            "expected {expected}, got {}",
461            bar.animation_time,
462        );
463    }
464
465    // ── set_theme ────────────────────────────────────────────────────────
466
467    #[test]
468    fn test_set_theme_does_not_panic() {
469        let mut bar = AnimatedProgress::new();
470        bar.set_theme(Theme::Light);
471        bar.set_theme(Theme::Dark);
472        // If we got here, set_theme works without panic.
473    }
474}