egui_circular_progress_bar/
lib.rs1use egui::{
2 pos2, Color32, Rect, Response, Sense, Stroke, Ui, Vec2, Widget, WidgetInfo, WidgetType,
3};
4use std::f32::consts::{FRAC_PI_2, TAU};
5
6pub struct CircularProgressBar {
8 progress: f32,
10 size: Option<f32>,
12 text: Option<String>,
14}
15
16impl CircularProgressBar {
17 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 pub fn size(mut self, size: f32) -> Self {
28 self.size = Some(size);
29 self
30 }
31
32 pub fn text(mut self, text: impl Into<String>) -> Self {
34 self.text = Some(text.into());
35 self
36 }
37
38 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 painter.circle_stroke(
84 center,
85 radius,
86 Stroke::new(stroke_width * 0.5, visuals.bg_stroke.color),
87 );
88
89 let start_angle = -FRAC_PI_2; let progress_angle = TAU * self.progress;
93 let end_angle = start_angle + progress_angle;
94
95 if self.progress > 0.0 {
97 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 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 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 ui.ctx().request_repaint();
142 }
143}
144
145pub trait CircularProgressBarExt {
147 fn circular_progress_bar(&mut self, progress: f32) -> Response;
149 fn circular_progress_bar_with_size(&mut self, progress: f32, size: f32) -> Response;
151 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}