Skip to main content

flashkraft_gui/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}