Skip to main content

egui_material3/
progress.rs

1use crate::get_global_color;
2use eframe::egui::{Color32, CornerRadius, Pos2, Rect, Response, Sense, Stroke, Ui, Vec2, Widget};
3use std::f32::consts::PI;
4
5// Animation duration constants (from Flutter reference)
6const INDETERMINATE_LINEAR_DURATION_MS: f32 = 1800.0;
7const INDETERMINATE_CIRCULAR_DURATION_MS: f32 = 1333.0 * 2.222;
8
9// Track gap ramp-down threshold: below this progress value, the gap is
10// scaled proportionally to prevent it from appearing abruptly at 0%.
11const TRACK_GAP_RAMP_DOWN_THRESHOLD: f32 = 0.01;
12
13/// Material Design progress indicator variants
14#[derive(Clone, Copy, PartialEq)]
15pub enum ProgressVariant {
16    /// Linear progress bar - horizontal bar showing progress
17    Linear,
18    /// Circular progress indicator - circular arc showing progress
19    Circular,
20}
21
22/// Material Design progress indicator component
23///
24/// Progress indicators inform users about the status of ongoing processes, such as
25/// loading an app, submitting a form, or saving updates. They communicate an app's
26/// state and indicate available actions.
27///
28/// ## Usage Examples
29/// ```rust
30/// # egui::__run_test_ui(|ui| {
31/// // Linear progress with value
32/// ui.add(MaterialProgress::linear()
33///     .value(0.65)
34///     .size(Vec2::new(300.0, 6.0)));
35///
36/// // Circular progress with value
37/// ui.add(MaterialProgress::circular()
38///     .value(0.8)
39///     .size(Vec2::splat(64.0)));
40///
41/// // Indeterminate linear progress (loading)
42/// ui.add(MaterialProgress::linear()
43///     .indeterminate(true));
44///
45/// // Buffered linear progress (like video loading)
46/// ui.add(MaterialProgress::linear()
47///     .value(0.3)
48///     .buffer(0.6));
49///
50/// // Customized colors and style
51/// ui.add(MaterialProgress::linear()
52///     .value(0.5)
53///     .active_color(Color32::RED)
54///     .track_color(Color32::LIGHT_GRAY)
55///     .track_gap(4.0)
56///     .stop_indicator_radius(2.0));
57/// # });
58/// ```
59///
60/// ## Material Design Spec
61/// - Linear: 4dp height (default), variable width
62/// - Circular: 48dp diameter (default), 4dp stroke width
63/// - Colors: Primary color for progress, secondaryContainer for track
64/// - Animation: Smooth transitions, indeterminate animations
65/// - Corner radius: pill-shaped for linear progress
66/// - Track gap: 4dp between indicator and track (M3 2024)
67/// - Stop indicator: 2dp radius dot at track end (linear determinate)
68pub struct MaterialProgress {
69    /// Type of progress indicator (linear or circular)
70    variant: ProgressVariant,
71    /// Current progress value (0.0 to max)
72    value: f32,
73    /// Maximum value for progress calculation
74    max: f32,
75    /// Optional buffer value for buffered progress (e.g., video loading)
76    buffer: Option<f32>,
77    /// Whether to show indeterminate progress animation
78    indeterminate: bool,
79    /// Whether to use four-color animation for indeterminate progress
80    four_color_enabled: bool,
81    /// Size of the progress indicator
82    size: Vec2,
83    /// Custom indicator/active color (default: primary)
84    active_color: Option<Color32>,
85    /// Custom track color (default: secondaryContainer)
86    track_color: Option<Color32>,
87    /// Custom buffer color (default: primaryContainer)
88    buffer_color: Option<Color32>,
89    /// Corner radius for linear progress (default: height / 2.0)
90    border_radius: Option<f32>,
91    /// Stroke width for circular progress (default: 4.0)
92    stroke_width: Option<f32>,
93    /// Gap between indicator and track (default: 4.0)
94    track_gap: Option<f32>,
95    /// Radius of the stop indicator dot at track end (default: 2.0, set 0 to hide)
96    stop_indicator_radius: Option<f32>,
97    /// Color of the stop indicator dot (default: primary)
98    stop_indicator_color: Option<Color32>,
99}
100
101impl MaterialProgress {
102    /// Create a new progress indicator with the specified variant
103    pub fn new(variant: ProgressVariant) -> Self {
104        Self {
105            variant,
106            value: 0.0,
107            max: 1.0,
108            buffer: None,
109            indeterminate: false,
110            four_color_enabled: false,
111            size: match variant {
112                ProgressVariant::Linear => Vec2::new(200.0, 4.0),
113                ProgressVariant::Circular => Vec2::splat(48.0),
114            },
115            active_color: None,
116            track_color: None,
117            buffer_color: None,
118            border_radius: None,
119            stroke_width: None,
120            track_gap: None,
121            stop_indicator_radius: None,
122            stop_indicator_color: None,
123        }
124    }
125
126    /// Create a linear progress bar
127    pub fn linear() -> Self {
128        Self::new(ProgressVariant::Linear)
129    }
130
131    /// Create a circular progress indicator
132    pub fn circular() -> Self {
133        Self::new(ProgressVariant::Circular)
134    }
135
136    /// Set the current progress value (clamped between 0.0 and max)
137    pub fn value(mut self, value: f32) -> Self {
138        self.value = value.clamp(0.0, self.max);
139        self
140    }
141
142    /// Set the maximum value for progress calculation (default: 1.0)
143    pub fn max(mut self, max: f32) -> Self {
144        self.max = max.max(0.001);
145        self.value = self.value.clamp(0.0, self.max);
146        self
147    }
148
149    /// Set the buffer value for buffered progress (e.g., video buffering)
150    pub fn buffer(mut self, buffer: f32) -> Self {
151        self.buffer = Some(buffer.clamp(0.0, self.max));
152        self
153    }
154
155    /// Enable or disable indeterminate progress animation
156    pub fn indeterminate(mut self, indeterminate: bool) -> Self {
157        self.indeterminate = indeterminate;
158        self
159    }
160
161    /// Enable or disable four-color animation for indeterminate progress
162    pub fn four_color_enabled(mut self, enabled: bool) -> Self {
163        self.four_color_enabled = enabled;
164        self
165    }
166
167    /// Set the size of the progress indicator
168    pub fn size(mut self, size: Vec2) -> Self {
169        self.size = size;
170        self
171    }
172
173    /// Set the width of the progress indicator
174    pub fn width(mut self, width: f32) -> Self {
175        self.size.x = width;
176        self
177    }
178
179    /// Set the height of the progress indicator
180    pub fn height(mut self, height: f32) -> Self {
181        self.size.y = height;
182        self
183    }
184
185    /// Set the indicator/active color (default: theme primary)
186    pub fn active_color(mut self, color: Color32) -> Self {
187        self.active_color = Some(color);
188        self
189    }
190
191    /// Set the track background color (default: theme secondaryContainer)
192    pub fn track_color(mut self, color: Color32) -> Self {
193        self.track_color = Some(color);
194        self
195    }
196
197    /// Set the buffer indicator color (default: theme primaryContainer)
198    pub fn buffer_color(mut self, color: Color32) -> Self {
199        self.buffer_color = Some(color);
200        self
201    }
202
203    /// Set the corner radius for linear progress (default: height / 2.0 for pill shape)
204    pub fn border_radius(mut self, radius: f32) -> Self {
205        self.border_radius = Some(radius);
206        self
207    }
208
209    /// Set the stroke width for circular progress (default: 4.0)
210    pub fn stroke_width(mut self, width: f32) -> Self {
211        self.stroke_width = Some(width);
212        self
213    }
214
215    /// Set the gap between indicator and track (default: 4.0, set 0 to hide)
216    pub fn track_gap(mut self, gap: f32) -> Self {
217        self.track_gap = Some(gap);
218        self
219    }
220
221    /// Set the stop indicator dot radius for linear determinate (default: 2.0, set 0 to hide)
222    pub fn stop_indicator_radius(mut self, radius: f32) -> Self {
223        self.stop_indicator_radius = Some(radius);
224        self
225    }
226
227    /// Set the stop indicator dot color (default: theme primary)
228    pub fn stop_indicator_color(mut self, color: Color32) -> Self {
229        self.stop_indicator_color = Some(color);
230        self
231    }
232
233    /// Enable or disable four-color animation (deprecated, use four_color_enabled)
234    #[deprecated(note = "Use four_color_enabled() instead")]
235    pub fn four_color(mut self, enabled: bool) -> Self {
236        self.four_color_enabled = enabled;
237        self
238    }
239}
240
241impl Widget for MaterialProgress {
242    fn ui(self, ui: &mut Ui) -> Response {
243        let (rect, response) = ui.allocate_exact_size(self.size, Sense::hover());
244
245        match self.variant {
246            ProgressVariant::Linear => self.render_linear(ui, rect),
247            ProgressVariant::Circular => self.render_circular(ui, rect),
248        }
249
250        response
251    }
252}
253
254// --- Cubic bezier interpolation for animation curves ---
255
256/// Evaluate a cubic bezier curve at parameter t.
257/// Control points: (0,0), (x1,y1), (x2,y2), (1,1)
258fn cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32, t: f32) -> f32 {
259    // Use Newton's method to find the t parameter for the given x value
260    // then evaluate y at that t.
261    // For animation curves, input t is the x-axis (time fraction).
262    let mut guess = t;
263    for _ in 0..8 {
264        let x = cubic_eval(x1, x2, guess) - t;
265        if x.abs() < 1e-6 {
266            break;
267        }
268        let dx = cubic_eval_derivative(x1, x2, guess);
269        if dx.abs() < 1e-6 {
270            break;
271        }
272        guess -= x / dx;
273        guess = guess.clamp(0.0, 1.0);
274    }
275    cubic_eval(y1, y2, guess)
276}
277
278fn cubic_eval(a: f32, b: f32, t: f32) -> f32 {
279    // Cubic bezier with points 0, a, b, 1
280    let t2 = t * t;
281    let t3 = t2 * t;
282    let mt = 1.0 - t;
283    let mt2 = mt * mt;
284    3.0 * mt2 * t * a + 3.0 * mt * t2 * b + t3
285}
286
287fn cubic_eval_derivative(a: f32, b: f32, t: f32) -> f32 {
288    let mt = 1.0 - t;
289    3.0 * mt * mt * a + 6.0 * mt * t * (b - a) + 3.0 * t * t * (1.0 - b)
290}
291
292/// Interval transform: maps t from [begin..end] to [0..1], clamped.
293fn interval(t: f32, begin: f32, end: f32) -> f32 {
294    ((t - begin) / (end - begin)).clamp(0.0, 1.0)
295}
296
297// --- Linear indeterminate animation curves (from Flutter) ---
298// Duration: 1800ms
299
300fn line1_head(t: f32) -> f32 {
301    let local_t = interval(t, 0.0, 750.0 / INDETERMINATE_LINEAR_DURATION_MS);
302    cubic_bezier(0.2, 0.0, 0.8, 1.0, local_t)
303}
304
305fn line1_tail(t: f32) -> f32 {
306    let local_t = interval(t, 333.0 / INDETERMINATE_LINEAR_DURATION_MS, 1083.0 / INDETERMINATE_LINEAR_DURATION_MS);
307    cubic_bezier(0.4, 0.0, 1.0, 1.0, local_t)
308}
309
310fn line2_head(t: f32) -> f32 {
311    let local_t = interval(t, 1000.0 / INDETERMINATE_LINEAR_DURATION_MS, 1567.0 / INDETERMINATE_LINEAR_DURATION_MS);
312    cubic_bezier(0.0, 0.0, 0.65, 1.0, local_t)
313}
314
315fn line2_tail(t: f32) -> f32 {
316    let local_t = interval(t, 1267.0 / INDETERMINATE_LINEAR_DURATION_MS, 1800.0 / INDETERMINATE_LINEAR_DURATION_MS);
317    cubic_bezier(0.10, 0.0, 0.45, 1.0, local_t)
318}
319
320// --- Circular indeterminate animation (from Flutter) ---
321
322const CIRCULAR_PATH_COUNT: f32 = 3.0;
323const CIRCULAR_ROTATION_COUNT: f32 = CIRCULAR_PATH_COUNT * 5.0 / 6.0;
324
325fn sawtooth(t: f32, count: f32) -> f32 {
326    (t * count).fract()
327}
328
329fn circular_head_value(t: f32) -> f32 {
330    let st = sawtooth(t, CIRCULAR_PATH_COUNT);
331    interval(st, 0.0, 0.5)
332}
333
334fn circular_tail_value(t: f32) -> f32 {
335    let st = sawtooth(t, CIRCULAR_PATH_COUNT);
336    interval(st, 0.5, 1.0)
337}
338
339fn circular_offset_value(t: f32) -> f32 {
340    sawtooth(t, CIRCULAR_PATH_COUNT)
341}
342
343fn circular_rotation_value(t: f32) -> f32 {
344    sawtooth(t, CIRCULAR_ROTATION_COUNT)
345}
346
347impl MaterialProgress {
348    /// Resolve colors with fallback to theme defaults
349    fn resolve_active_color(&self) -> Color32 {
350        self.active_color.unwrap_or_else(|| get_global_color("primary"))
351    }
352
353    fn resolve_track_color(&self) -> Color32 {
354        self.track_color.unwrap_or_else(|| get_global_color("secondaryContainer"))
355    }
356
357    fn resolve_buffer_color(&self) -> Color32 {
358        self.buffer_color.unwrap_or_else(|| get_global_color("primaryContainer"))
359    }
360
361    fn resolve_stop_indicator_color(&self) -> Color32 {
362        self.stop_indicator_color.unwrap_or_else(|| get_global_color("primary"))
363    }
364
365    fn resolve_border_radius(&self, rect_height: f32) -> f32 {
366        self.border_radius.unwrap_or(rect_height / 2.0)
367    }
368
369    fn resolve_stroke_width(&self) -> f32 {
370        self.stroke_width.unwrap_or(4.0)
371    }
372
373    fn resolve_track_gap(&self) -> f32 {
374        self.track_gap.unwrap_or(4.0)
375    }
376
377    fn resolve_stop_indicator_radius(&self, rect_height: f32) -> f32 {
378        let r = self.stop_indicator_radius.unwrap_or(2.0);
379        r.min(rect_height / 2.0)
380    }
381
382    /// Get effective track gap fraction scaled proportionally near 0%.
383    fn effective_track_gap_fraction(current_value: f32, track_gap_fraction: f32) -> f32 {
384        track_gap_fraction
385            * current_value.clamp(0.0, TRACK_GAP_RAMP_DOWN_THRESHOLD)
386            / TRACK_GAP_RAMP_DOWN_THRESHOLD
387    }
388
389    /// Get the four-color cycle color based on animation time
390    fn get_four_color(&self, time: f32) -> Color32 {
391        let colors = [
392            get_global_color("primary"),
393            get_global_color("primaryContainer"),
394            get_global_color("tertiary"),
395            get_global_color("tertiaryContainer"),
396        ];
397        let cycle = (time * 0.5) as usize % 4; // Change color roughly every 2 seconds
398        colors[cycle]
399    }
400
401    fn render_linear(&self, ui: &mut Ui, rect: Rect) {
402        let active_color = if self.four_color_enabled && self.indeterminate {
403            let time = ui.input(|i| i.time) as f32;
404            self.get_four_color(time)
405        } else {
406            self.resolve_active_color()
407        };
408        let track_color = self.resolve_track_color();
409        let buffer_color = self.resolve_buffer_color();
410        let border_radius = self.resolve_border_radius(rect.height());
411        let rounding = CornerRadius::same(border_radius as u8);
412        let track_gap = self.resolve_track_gap();
413        let track_gap_fraction = track_gap / rect.width();
414
415        if self.indeterminate {
416            // Flutter-style dual-bar indeterminate animation
417            let time = ui.input(|i| i.time) as f32;
418            let cycle_duration = INDETERMINATE_LINEAR_DURATION_MS / 1000.0;
419            let animation_value = ((time % cycle_duration) / cycle_duration).clamp(0.0, 1.0);
420
421            let first_line_head = line1_head(animation_value);
422            let first_line_tail = line1_tail(animation_value);
423            let second_line_head = line2_head(animation_value);
424            let second_line_tail = line2_tail(animation_value);
425
426            // Draw track before line 1 (right side of line 1)
427            if first_line_head < 1.0 - track_gap_fraction {
428                let track_start = if first_line_head > 0.0 {
429                    first_line_head + Self::effective_track_gap_fraction(first_line_head, track_gap_fraction)
430                } else {
431                    0.0
432                };
433                self.draw_linear_segment(ui, rect, track_start, 1.0, track_color, rounding);
434            }
435
436            // Draw line 1
437            if first_line_head - first_line_tail > 0.0 {
438                self.draw_linear_segment(ui, rect, first_line_tail, first_line_head, active_color, rounding);
439            }
440
441            // Draw track between line 1 and line 2
442            if first_line_tail > track_gap_fraction {
443                let track_start = if second_line_head > 0.0 {
444                    second_line_head + Self::effective_track_gap_fraction(second_line_head, track_gap_fraction)
445                } else {
446                    0.0
447                };
448                let track_end = if first_line_tail < 1.0 {
449                    first_line_tail - Self::effective_track_gap_fraction(1.0 - first_line_tail, track_gap_fraction)
450                } else {
451                    1.0
452                };
453                if track_end > track_start {
454                    self.draw_linear_segment(ui, rect, track_start, track_end, track_color, rounding);
455                }
456            }
457
458            // Draw line 2
459            if second_line_head - second_line_tail > 0.0 {
460                self.draw_linear_segment(ui, rect, second_line_tail, second_line_head, active_color, rounding);
461            }
462
463            // Draw track after line 2 (left side of line 2)
464            if second_line_tail > track_gap_fraction {
465                let track_end = if second_line_tail < 1.0 {
466                    second_line_tail - Self::effective_track_gap_fraction(1.0 - second_line_tail, track_gap_fraction)
467                } else {
468                    1.0
469                };
470                self.draw_linear_segment(ui, rect, 0.0, track_end, track_color, rounding);
471            }
472
473            // If both lines haven't started yet, draw full track
474            if first_line_head <= 0.0 && second_line_head <= 0.0 {
475                self.draw_linear_segment(ui, rect, 0.0, 1.0, track_color, rounding);
476            }
477
478            ui.ctx().request_repaint();
479        } else {
480            // Determinate progress
481            let progress = (self.value / self.max).clamp(0.0, 1.0);
482
483            // Draw track with gap
484            let track_start = if track_gap_fraction > 0.0 && progress > 0.0 {
485                progress + Self::effective_track_gap_fraction(progress, track_gap_fraction)
486            } else {
487                0.0
488            };
489            if track_start < 1.0 {
490                self.draw_linear_segment(ui, rect, track_start, 1.0, track_color, rounding);
491            }
492
493            // Draw stop indicator at the end of the track
494            let stop_radius = self.resolve_stop_indicator_radius(rect.height());
495            if stop_radius > 0.0 {
496                let stop_color = self.resolve_stop_indicator_color();
497                let max_radius = rect.height() / 2.0;
498                let center = Pos2::new(
499                    rect.max.x - max_radius,
500                    rect.min.y + max_radius,
501                );
502                ui.painter().circle_filled(center, stop_radius, stop_color);
503            }
504
505            // Draw buffer if present
506            if let Some(buffer) = self.buffer {
507                let buffer_progress = (buffer / self.max).clamp(0.0, 1.0);
508                if buffer_progress > progress {
509                    let buffer_start = if track_gap_fraction > 0.0 && progress > 0.0 {
510                        progress + Self::effective_track_gap_fraction(progress, track_gap_fraction)
511                    } else {
512                        progress
513                    };
514                    if buffer_progress > buffer_start {
515                        self.draw_linear_segment(ui, rect, buffer_start, buffer_progress, buffer_color, rounding);
516                    }
517                }
518            }
519
520            // Draw progress bar
521            if progress > 0.0 {
522                self.draw_linear_segment(ui, rect, 0.0, progress, active_color, rounding);
523            }
524        }
525    }
526
527    fn draw_linear_segment(
528        &self,
529        ui: &mut Ui,
530        rect: Rect,
531        start_fraction: f32,
532        end_fraction: f32,
533        color: Color32,
534        rounding: CornerRadius,
535    ) {
536        if end_fraction - start_fraction <= 0.0 {
537            return;
538        }
539
540        let left = rect.min.x + start_fraction * rect.width();
541        let right = rect.min.x + end_fraction * rect.width();
542        let segment_rect = Rect::from_min_max(
543            Pos2::new(left, rect.min.y),
544            Pos2::new(right, rect.max.y),
545        );
546
547        ui.painter().rect_filled(segment_rect, rounding, color);
548    }
549
550    fn render_circular(&self, ui: &mut Ui, rect: Rect) {
551        let stroke_width = self.resolve_stroke_width();
552        let center = rect.center();
553        let radius = (rect.width().min(rect.height()) / 2.0) - stroke_width / 2.0;
554        let track_color = self.resolve_track_color();
555        let track_gap = self.resolve_track_gap();
556
557        if self.indeterminate {
558            let time = ui.input(|i| i.time) as f32;
559            let cycle_duration = INDETERMINATE_CIRCULAR_DURATION_MS / 1000.0;
560            let animation_value = ((time % cycle_duration) / cycle_duration).clamp(0.0, 1.0);
561
562            let head_value = circular_head_value(animation_value);
563            let tail_value = circular_tail_value(animation_value);
564            let offset_value = circular_offset_value(animation_value);
565            let rotation_value = circular_rotation_value(animation_value);
566
567            // Draw track (full circle, no gap for indeterminate)
568            ui.painter().circle_stroke(center, radius, Stroke::new(stroke_width, track_color));
569
570            // Calculate arc start and sweep (from Flutter reference)
571            let arc_start = -PI / 2.0
572                + tail_value * 3.0 / 2.0 * PI
573                + rotation_value * PI * 2.0
574                + offset_value * 0.5 * PI;
575            let arc_sweep = (head_value * 3.0 / 2.0 * PI - tail_value * 3.0 / 2.0 * PI).max(0.001);
576
577            let active_color = if self.four_color_enabled {
578                self.get_four_color(time)
579            } else {
580                self.resolve_active_color()
581            };
582
583            self.draw_arc(
584                ui,
585                center,
586                radius,
587                arc_start,
588                arc_start + arc_sweep,
589                stroke_width,
590                active_color,
591            );
592
593            ui.ctx().request_repaint();
594        } else {
595            let progress = (self.value / self.max).clamp(0.0, 1.0);
596            let active_color = self.resolve_active_color();
597
598            let epsilon = 0.001;
599            let two_pi = 2.0 * PI;
600
601            if track_gap > 0.0 && progress > epsilon {
602                // Draw track with gap (from Flutter reference)
603                let arc_radius = radius;
604                let stroke_radius = stroke_width / arc_radius;
605                let gap_radius = track_gap / arc_radius;
606                let start_gap = stroke_radius + gap_radius;
607                let end_gap = if progress < epsilon { start_gap } else { start_gap * 2.0 };
608                let track_start = -PI / 2.0 + start_gap;
609                let track_sweep = (two_pi - progress.clamp(0.0, 1.0) * two_pi - end_gap).max(0.0);
610
611                if track_sweep > 0.0 {
612                    // Draw the track arc on the opposite side (flipped)
613                    let flipped_start = PI - track_start;
614                    self.draw_arc(
615                        ui,
616                        center,
617                        radius,
618                        flipped_start,
619                        flipped_start - track_sweep,
620                        stroke_width,
621                        track_color,
622                    );
623                }
624            } else {
625                // Full track circle (no gap)
626                ui.painter().circle_stroke(center, radius, Stroke::new(stroke_width, track_color));
627            }
628
629            // Draw progress arc
630            if progress > 0.0 {
631                let arc_length = two_pi * progress - epsilon;
632                self.draw_arc(
633                    ui,
634                    center,
635                    radius,
636                    -PI / 2.0,
637                    -PI / 2.0 + arc_length,
638                    stroke_width,
639                    active_color,
640                );
641            }
642        }
643    }
644
645    #[allow(clippy::too_many_arguments)]
646    fn draw_arc(
647        &self,
648        ui: &mut Ui,
649        center: Pos2,
650        radius: f32,
651        start_angle: f32,
652        end_angle: f32,
653        stroke_width: f32,
654        color: Color32,
655    ) {
656        let segments = 48;
657        let angle_step = (end_angle - start_angle) / segments as f32;
658
659        for i in 0..segments {
660            let angle1 = start_angle + i as f32 * angle_step;
661            let angle2 = start_angle + (i + 1) as f32 * angle_step;
662
663            let point1 = Pos2::new(
664                center.x + radius * angle1.cos(),
665                center.y + radius * angle1.sin(),
666            );
667            let point2 = Pos2::new(
668                center.x + radius * angle2.cos(),
669                center.y + radius * angle2.sin(),
670            );
671
672            ui.painter()
673                .line_segment([point1, point2], Stroke::new(stroke_width, color));
674        }
675    }
676}
677
678pub fn linear_progress() -> MaterialProgress {
679    MaterialProgress::linear()
680}
681
682pub fn circular_progress() -> MaterialProgress {
683    MaterialProgress::circular()
684}