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}