egui_material3/
progress.rs

1use eframe::egui::{Color32, Pos2, Rect, Response, Sense, Stroke, Ui, Vec2, Widget};
2use crate::get_global_color;
3use std::f32::consts::PI;
4
5/// Material Design progress indicator variants
6#[derive(Clone, Copy, PartialEq)]
7pub enum ProgressVariant {
8    /// Linear progress bar - horizontal bar showing progress
9    Linear,
10    /// Circular progress indicator - circular arc showing progress
11    Circular,
12}
13
14/// Material Design progress indicator component
15///
16/// Progress indicators inform users about the status of ongoing processes, such as
17/// loading an app, submitting a form, or saving updates. They communicate an app's
18/// state and indicate available actions.
19///
20/// ## Usage Examples
21/// ```rust
22/// # egui::__run_test_ui(|ui| {
23/// // Linear progress with value
24/// ui.add(MaterialProgress::linear()
25///     .value(0.65)
26///     .size(Vec2::new(300.0, 6.0)));
27/// 
28/// // Circular progress with value
29/// ui.add(MaterialProgress::circular()
30///     .value(0.8)
31///     .size(Vec2::splat(64.0)));
32/// 
33/// // Indeterminate linear progress (loading)
34/// ui.add(MaterialProgress::linear()
35///     .indeterminate(true));
36/// 
37/// // Buffered linear progress (like video loading)
38/// ui.add(MaterialProgress::linear()
39///     .value(0.3)
40///     .buffer(0.6));
41/// # });
42/// ```
43///
44/// ## Material Design Spec
45/// - Linear: 4dp height (default), variable width
46/// - Circular: 48dp diameter (default), 4dp stroke width
47/// - Colors: Primary color for progress, surfaceVariant for track
48/// - Animation: Smooth transitions, indeterminate animations
49/// - Corner radius: 2dp for linear progress
50pub struct MaterialProgress {
51    /// Type of progress indicator (linear or circular)
52    variant: ProgressVariant,
53    /// Current progress value (0.0 to max)
54    value: f32,
55    /// Maximum value for progress calculation
56    max: f32,
57    /// Optional buffer value for buffered progress (e.g., video loading)
58    buffer: Option<f32>,
59    /// Whether to show indeterminate progress animation
60    indeterminate: bool,
61    /// Whether to use four-color animation for indeterminate progress
62    four_color_enabled: bool,
63    /// Size of the progress indicator
64    size: Vec2,
65}
66
67impl MaterialProgress {
68    /// Create a new progress indicator with the specified variant
69    /// 
70    /// ## Parameters
71    /// - `variant`: Whether to create a linear or circular progress indicator
72    /// 
73    /// ## Returns
74    /// A new MaterialProgress instance with default settings
75    pub fn new(variant: ProgressVariant) -> Self {
76        Self {
77            variant,
78            value: 0.0,
79            max: 1.0,
80            buffer: None,
81            indeterminate: false,
82            four_color_enabled: false,
83            size: match variant {
84                ProgressVariant::Linear => Vec2::new(200.0, 4.0),
85                ProgressVariant::Circular => Vec2::splat(48.0),
86            },
87        }
88    }
89
90    /// Create a linear progress bar
91    /// 
92    /// Linear progress indicators display progress along a horizontal line.
93    /// Best for showing progress of tasks with known duration.
94    /// 
95    /// ## Material Design Usage
96    /// - File downloads/uploads
97    /// - Form submission progress  
98    /// - Loading content with known steps
99    pub fn linear() -> Self {
100        Self::new(ProgressVariant::Linear)
101    }
102
103    /// Create a circular progress indicator
104    /// 
105    /// Circular progress indicators display progress along a circular path.
106    /// Best for compact spaces or indeterminate progress.
107    /// 
108    /// ## Material Design Usage
109    /// - Loading states in buttons or cards
110    /// - Refreshing content
111    /// - Background processing
112    pub fn circular() -> Self {
113        Self::new(ProgressVariant::Circular)
114    }
115
116    /// Set the current progress value
117    /// 
118    /// ## Parameters
119    /// - `value`: Progress value (will be clamped between 0.0 and max)
120    pub fn value(mut self, value: f32) -> Self {
121        self.value = value.clamp(0.0, self.max);
122        self
123    }
124
125    /// Set the maximum value for progress calculation
126    /// 
127    /// The progress percentage will be calculated as value/max.
128    /// 
129    /// ## Parameters
130    /// - `max`: Maximum value (default is 1.0 for 0-100% range)
131    pub fn max(mut self, max: f32) -> Self {
132        self.max = max.max(0.001); // Prevent division by zero
133        self.value = self.value.clamp(0.0, self.max);
134        self
135    }
136
137    /// Set the buffer value for buffered progress
138    /// 
139    /// Buffered progress shows an additional value indicating estimated completion.
140    /// Useful for tasks like video buffering where loading status is variable.
141    /// 
142    /// ## Parameters
143    /// - `buffer`: Buffer value (will be clamped between 0.0 and max)
144    pub fn buffer(mut self, buffer: f32) -> Self {
145        self.buffer = Some(buffer.clamp(0.0, self.max));
146        self
147    }
148
149    /// Enable or disable indeterminate progress animation
150    /// 
151    /// Indeterminate progress is used when the actual progress is unknown,
152    /// such as during loading states. It shows a looping animation to indicate
153    /// activity.
154    /// 
155    /// ## Parameters
156    /// - `indeterminate`: true to enable indeterminate mode, false to disable
157    pub fn indeterminate(mut self, indeterminate: bool) -> Self {
158        self.indeterminate = indeterminate;
159        self
160    }
161
162    /// Enable or disable four-color animation for indeterminate progress
163    /// 
164    /// Four-color animation provides a more dynamic indeterminate animation
165    /// using four distinct colors. This can be visually appealing but may
166    /// impact performance due to increased draw calls.
167    /// 
168    /// ## Parameters
169    /// - `enabled`: true to enable four-color animation, false to disable
170    pub fn four_color_enabled(mut self, enabled: bool) -> Self {
171        self.four_color_enabled = enabled;
172        self
173    }
174
175    /// Set the size of the progress indicator
176    /// 
177    /// ## Parameters
178    /// - `size`: Desired size (width, height) of the progress indicator
179    pub fn size(mut self, size: Vec2) -> Self {
180        self.size = size;
181        self
182    }
183
184    /// Set the width of the progress indicator (for linear variant)
185    /// 
186    /// ## Parameters
187    /// - `width`: Desired width of the progress indicator
188    pub fn width(mut self, width: f32) -> Self {
189        self.size.x = width;
190        self
191    }
192
193    /// Set the height of the progress indicator (for circular variant)
194    /// 
195    /// ## Parameters
196    /// - `height`: Desired height of the progress indicator
197    pub fn height(mut self, height: f32) -> Self {
198        self.size.y = height;
199        self
200    }
201
202    /// Enable or disable four-color animation (deprecated, use four_color_enabled)
203    /// 
204    /// ## Parameters
205    /// - `enabled`: true to enable four-color animation, false to disable
206    #[deprecated(note = "Use four_color_enabled() instead")]
207    pub fn four_color(mut self, enabled: bool) -> Self {
208        self.four_color_enabled = enabled;
209        self
210    }
211}
212
213impl Widget for MaterialProgress {
214    fn ui(self, ui: &mut Ui) -> Response {
215        let (rect, response) = ui.allocate_exact_size(self.size, Sense::hover());
216
217        match self.variant {
218            ProgressVariant::Linear => self.render_linear(ui, rect),
219            ProgressVariant::Circular => self.render_circular(ui, rect),
220        }
221
222        response
223    }
224}
225
226impl MaterialProgress {
227    fn render_linear(&self, ui: &mut Ui, rect: Rect) {
228        // Material Design colors
229        let primary_color = get_global_color("primary");
230        let surface_variant = get_global_color("surfaceVariant");
231        let primary_container = get_global_color("primaryContainer");
232
233        // Draw track background
234        ui.painter().rect_filled(
235            rect,
236            rect.height() / 2.0,
237            surface_variant,
238        );
239
240        if self.indeterminate {
241            // Indeterminate animation - simplified for egui
242            let time = ui.input(|i| i.time) as f32;
243            let progress = ((time * 2.0).sin() + 1.0) / 2.0; // Oscillate between 0 and 1
244            
245            let bar_width = rect.width() * 0.3; // 30% of total width
246            let start_x = rect.min.x + (rect.width() - bar_width) * progress;
247            
248            let bar_rect = Rect::from_min_size(
249                Pos2::new(start_x, rect.min.y),
250                Vec2::new(bar_width, rect.height()),
251            );
252            
253            ui.painter().rect_filled(
254                bar_rect,
255                rect.height() / 2.0,
256                primary_color,
257            );
258            
259            // Request repaint for animation
260            ui.ctx().request_repaint();
261        } else {
262            // Draw buffer if present
263            if let Some(buffer) = self.buffer {
264                let buffer_progress = (buffer / self.max).clamp(0.0, 1.0);
265                let buffer_width = rect.width() * buffer_progress;
266                
267                if buffer_width > 0.0 {
268                    let buffer_rect = Rect::from_min_size(
269                        rect.min,
270                        Vec2::new(buffer_width, rect.height()),
271                    );
272                    
273                    ui.painter().rect_filled(
274                        buffer_rect,
275                        rect.height() / 2.0,
276                        primary_container,
277                    );
278                }
279            }
280
281            // Draw progress bar
282            let progress = (self.value / self.max).clamp(0.0, 1.0);
283            let progress_width = rect.width() * progress;
284            
285            if progress_width > 0.0 {
286                let progress_rect = Rect::from_min_size(
287                    rect.min,
288                    Vec2::new(progress_width, rect.height()),
289                );
290                
291                ui.painter().rect_filled(
292                    progress_rect,
293                    rect.height() / 2.0,
294                    primary_color,
295                );
296            }
297        }
298    }
299
300    fn render_circular(&self, ui: &mut Ui, rect: Rect) {
301        let center = rect.center();
302        let radius = (rect.width().min(rect.height()) / 2.0) - 4.0;
303        let stroke_width = 4.0;
304
305        // Material Design colors
306        let primary_color = get_global_color("primary");
307        let surface_variant = get_global_color("surfaceVariant");
308
309        // Draw track circle
310        ui.painter().circle_stroke(
311            center,
312            radius,
313            Stroke::new(stroke_width, surface_variant),
314        );
315
316        if self.indeterminate {
317            // Indeterminate animation - spinning arc
318            let time = ui.input(|i| i.time) as f32;
319            let rotation = time * 2.0; // Rotation speed
320            let arc_length = PI; // Half circle arc
321
322            self.draw_arc(
323                ui,
324                center,
325                radius,
326                rotation,
327                rotation + arc_length,
328                stroke_width,
329                primary_color,
330            );
331
332            // Request repaint for animation
333            ui.ctx().request_repaint();
334        } else {
335            // Draw progress arc
336            let progress = (self.value / self.max).clamp(0.0, 1.0);
337            let arc_length = 2.0 * PI * progress;
338            
339            if progress > 0.0 {
340                self.draw_arc(
341                    ui,
342                    center,
343                    radius,
344                    -PI / 2.0, // Start at top
345                    -PI / 2.0 + arc_length,
346                    stroke_width,
347                    primary_color,
348                );
349            }
350        }
351    }
352
353    fn draw_arc(
354        &self,
355        ui: &mut Ui,
356        center: Pos2,
357        radius: f32,
358        start_angle: f32,
359        end_angle: f32,
360        stroke_width: f32,
361        color: Color32,
362    ) {
363        let segments = 32;
364        let angle_step = (end_angle - start_angle) / segments as f32;
365        
366        for i in 0..segments {
367            let angle1 = start_angle + i as f32 * angle_step;
368            let angle2 = start_angle + (i + 1) as f32 * angle_step;
369            
370            let point1 = Pos2::new(
371                center.x + radius * angle1.cos(),
372                center.y + radius * angle1.sin(),
373            );
374            let point2 = Pos2::new(
375                center.x + radius * angle2.cos(),
376                center.y + radius * angle2.sin(),
377            );
378            
379            ui.painter().line_segment(
380                [point1, point2],
381                Stroke::new(stroke_width, color),
382            );
383        }
384    }
385}
386
387pub fn linear_progress() -> MaterialProgress {
388    MaterialProgress::linear()
389}
390
391pub fn circular_progress() -> MaterialProgress {
392    MaterialProgress::circular()
393}