egui_circular_progress_bar/
lib.rs

1use egui::{
2    pos2, Color32, Rect, Response, Sense, Stroke, Ui, Vec2, Widget, WidgetInfo, WidgetType,
3};
4use std::f32::consts::{FRAC_PI_2, TAU};
5
6/// A circular progress bar widget for egui
7pub struct CircularProgressBar {
8    /// Progress value from 0.0 to 1.0
9    progress: f32,
10    /// Optional size (diameter) of the progress bar
11    size: Option<f32>,
12    /// Optional text to display in the center of the progress bar
13    text: Option<String>,
14}
15
16impl CircularProgressBar {
17    /// Create a new circular progress bar with the given progress (0.0 to 1.0)
18    pub fn new(progress: f32) -> Self {
19        Self {
20            progress: progress.clamp(0.0, 1.0),
21            size: None,
22            text: None,
23        }
24    }
25
26    /// Set the size (diameter) of the circular progress bar
27    pub fn size(mut self, size: f32) -> Self {
28        self.size = Some(size);
29        self
30    }
31
32    /// Add text to display in the center of the progress bar
33    pub fn text(mut self, text: impl Into<String>) -> Self {
34        self.text = Some(text.into());
35        self
36    }
37
38    /// Create an indeterminate progress bar (animated)
39    pub fn indeterminate() -> Self {
40        Self {
41            progress: 0.0,
42            size: None,
43            text: None,
44        }
45    }
46}
47
48impl Widget for CircularProgressBar {
49    fn ui(self, ui: &mut Ui) -> Response {
50        let size = self.size.unwrap_or(ui.spacing().interact_size.y);
51        let (rect, response) = ui.allocate_exact_size(Vec2::splat(size), Sense::hover());
52
53        if ui.is_rect_visible(rect) {
54            self.paint_at(ui, rect);
55        }
56
57        if let Some(text) = &self.text {
58            response.widget_info(|| WidgetInfo::labeled(WidgetType::ProgressIndicator, true, text));
59        } else {
60            response.widget_info(|| {
61                WidgetInfo::labeled(
62                    WidgetType::ProgressIndicator,
63                    true,
64                    format!("{:.0}%", self.progress * 100.0),
65                )
66            });
67        }
68
69        response
70    }
71}
72
73impl CircularProgressBar {
74    fn paint_at(&self, ui: &Ui, rect: Rect) {
75        let visuals = &ui.visuals().widgets.inactive;
76        let painter = ui.painter_at(rect);
77
78        let center = rect.center();
79        let radius = rect.width().min(rect.height()) * 0.5 - 2.0;
80        let stroke_width = (radius * 0.1).max(2.0).min(4.0);
81
82        // Background circle
83        painter.circle_stroke(
84            center,
85            radius,
86            Stroke::new(stroke_width * 0.5, visuals.bg_stroke.color),
87        );
88
89        // Progress calculation
90
91        let start_angle = -FRAC_PI_2; // Start at top (12 o'clock)
92        let progress_angle = TAU * self.progress;
93        let end_angle = start_angle + progress_angle;
94
95        // Progress arc
96        if self.progress > 0.0 {
97            // Interpolate between stroke color and selection color
98            let from = visuals.fg_stroke.color;
99            let to = ui.visuals().selection.bg_fill;
100            let progress_color = Color32::from_rgba_premultiplied(
101                (from.r() as f32 + (to.r() as f32 - from.r() as f32) * self.progress) as u8,
102                (from.g() as f32 + (to.g() as f32 - from.g() as f32) * self.progress) as u8,
103                (from.b() as f32 + (to.b() as f32 - from.b() as f32) * self.progress) as u8,
104                (from.a() as f32 + (to.a() as f32 - from.a() as f32) * self.progress) as u8,
105            );
106
107            // Draw arc using path
108            let mut points = Vec::new();
109            let num_segments = ((end_angle - start_angle).abs() * radius / 2.0).ceil() as usize;
110            let num_segments = num_segments.max(4);
111
112            for i in 0..=num_segments {
113                let angle =
114                    start_angle + (end_angle - start_angle) * (i as f32 / num_segments as f32);
115                let x = center.x + radius * angle.cos();
116                let y = center.y + radius * angle.sin();
117                points.push(pos2(x, y));
118            }
119
120            for i in 0..points.len() - 1 {
121                painter.line_segment(
122                    [points[i], points[i + 1]],
123                    Stroke::new(stroke_width, progress_color),
124                );
125            }
126        }
127
128        // Center text
129        if let Some(text) = &self.text {
130            let text_color = ui.visuals().text_color();
131            painter.text(
132                center,
133                egui::Align2::CENTER_CENTER,
134                text,
135                egui::FontId::default(),
136                text_color,
137            );
138        }
139
140        // Request repaint for animation
141        ui.ctx().request_repaint();
142    }
143}
144
145/// Extension trait for Ui to add circular progress bar methods
146pub trait CircularProgressBarExt {
147    /// Add a circular progress bar
148    fn circular_progress_bar(&mut self, progress: f32) -> Response;
149    /// Add a circular progress bar with custom size
150    fn circular_progress_bar_with_size(&mut self, progress: f32, size: f32) -> Response;
151    /// Add an indeterminate circular progress bar (animated)
152    fn circular_progress_bar_indeterminate(&mut self) -> Response;
153}
154
155impl CircularProgressBarExt for Ui {
156    fn circular_progress_bar(&mut self, progress: f32) -> Response {
157        self.add(CircularProgressBar::new(progress))
158    }
159
160    fn circular_progress_bar_with_size(&mut self, progress: f32, size: f32) -> Response {
161        self.add(CircularProgressBar::new(progress).size(size))
162    }
163
164    fn circular_progress_bar_indeterminate(&mut self) -> Response {
165        self.add(CircularProgressBar::indeterminate())
166    }
167}