use egui::{
pos2, Color32, Rect, Response, Sense, Stroke, Ui, Vec2, Widget, WidgetInfo, WidgetType,
};
use std::f32::consts::{FRAC_PI_2, TAU};
pub struct CircularProgressBar {
progress: f32,
size: Option<f32>,
text: Option<String>,
}
impl CircularProgressBar {
pub fn new(progress: f32) -> Self {
Self {
progress: progress.clamp(0.0, 1.0),
size: None,
text: None,
}
}
pub fn size(mut self, size: f32) -> Self {
self.size = Some(size);
self
}
pub fn text(mut self, text: impl Into<String>) -> Self {
self.text = Some(text.into());
self
}
pub fn indeterminate() -> Self {
Self {
progress: 0.0,
size: None,
text: None,
}
}
}
impl Widget for CircularProgressBar {
fn ui(self, ui: &mut Ui) -> Response {
let size = self.size.unwrap_or(ui.spacing().interact_size.y);
let (rect, response) = ui.allocate_exact_size(Vec2::splat(size), Sense::hover());
if ui.is_rect_visible(rect) {
self.paint_at(ui, rect);
}
if let Some(text) = &self.text {
response.widget_info(|| WidgetInfo::labeled(WidgetType::ProgressIndicator, true, text));
} else {
response.widget_info(|| {
WidgetInfo::labeled(
WidgetType::ProgressIndicator,
true,
format!("{:.0}%", self.progress * 100.0),
)
});
}
response
}
}
impl CircularProgressBar {
fn paint_at(&self, ui: &Ui, rect: Rect) {
let visuals = &ui.visuals().widgets.inactive;
let painter = ui.painter_at(rect);
let center = rect.center();
let radius = rect.width().min(rect.height()) * 0.5 - 2.0;
let stroke_width = (radius * 0.1).max(2.0).min(4.0);
painter.circle_stroke(
center,
radius,
Stroke::new(stroke_width * 0.5, visuals.bg_stroke.color),
);
let start_angle = -FRAC_PI_2; let progress_angle = TAU * self.progress;
let end_angle = start_angle + progress_angle;
if self.progress > 0.0 {
let from = visuals.fg_stroke.color;
let to = ui.visuals().selection.bg_fill;
let progress_color = Color32::from_rgba_premultiplied(
(from.r() as f32 + (to.r() as f32 - from.r() as f32) * self.progress) as u8,
(from.g() as f32 + (to.g() as f32 - from.g() as f32) * self.progress) as u8,
(from.b() as f32 + (to.b() as f32 - from.b() as f32) * self.progress) as u8,
(from.a() as f32 + (to.a() as f32 - from.a() as f32) * self.progress) as u8,
);
let mut points = Vec::new();
let num_segments = ((end_angle - start_angle).abs() * radius / 2.0).ceil() as usize;
let num_segments = num_segments.max(4);
for i in 0..=num_segments {
let angle =
start_angle + (end_angle - start_angle) * (i as f32 / num_segments as f32);
let x = center.x + radius * angle.cos();
let y = center.y + radius * angle.sin();
points.push(pos2(x, y));
}
for i in 0..points.len() - 1 {
painter.line_segment(
[points[i], points[i + 1]],
Stroke::new(stroke_width, progress_color),
);
}
}
if let Some(text) = &self.text {
let text_color = ui.visuals().text_color();
painter.text(
center,
egui::Align2::CENTER_CENTER,
text,
egui::FontId::default(),
text_color,
);
}
ui.ctx().request_repaint();
}
}
pub trait CircularProgressBarExt {
fn circular_progress_bar(&mut self, progress: f32) -> Response;
fn circular_progress_bar_with_size(&mut self, progress: f32, size: f32) -> Response;
fn circular_progress_bar_indeterminate(&mut self) -> Response;
}
impl CircularProgressBarExt for Ui {
fn circular_progress_bar(&mut self, progress: f32) -> Response {
self.add(CircularProgressBar::new(progress))
}
fn circular_progress_bar_with_size(&mut self, progress: f32, size: f32) -> Response {
self.add(CircularProgressBar::new(progress).size(size))
}
fn circular_progress_bar_indeterminate(&mut self) -> Response {
self.add(CircularProgressBar::indeterminate())
}
}