use egui::{Color32, FontId, Response, Sense, Stroke, StrokeKind, Ui, Widget, vec2};
use super::{alpha, corner};
use crate::{RADIUS, palette_of};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GaugeShape {
Ring,
Bar,
}
#[derive(Debug, Clone, Copy)]
pub struct Thresholds {
pub warn_at: f32,
pub error_at: f32,
}
impl Default for Thresholds {
fn default() -> Self {
Self {
warn_at: 0.75,
error_at: 0.9,
}
}
}
pub struct Gauge<'a> {
value: f32,
shape: GaugeShape,
color: Option<Color32>,
label: Option<&'a str>,
show_percent: bool,
thresholds: Thresholds,
size: f32,
}
impl<'a> Gauge<'a> {
pub fn ring(value: f32) -> Self {
Self {
value: value.clamp(0.0, 1.0),
shape: GaugeShape::Ring,
color: None,
label: None,
show_percent: true,
thresholds: Thresholds::default(),
size: 64.0,
}
}
pub fn bar(value: f32) -> Self {
Self {
value: value.clamp(0.0, 1.0),
shape: GaugeShape::Bar,
color: None,
label: None,
show_percent: true,
thresholds: Thresholds::default(),
size: 8.0, }
}
pub fn label(mut self, label: &'a str) -> Self {
self.label = Some(label);
self
}
pub fn no_percent(mut self) -> Self {
self.show_percent = false;
self
}
pub fn color(mut self, color: Color32) -> Self {
self.color = Some(color);
self
}
pub fn thresholds(mut self, t: Thresholds) -> Self {
self.thresholds = t;
self
}
pub fn size(mut self, size: f32) -> Self {
self.size = size;
self
}
fn auto_color(&self, p: &crate::Palette) -> Color32 {
if let Some(c) = self.color {
return c;
}
if self.value >= self.thresholds.error_at {
p.error
} else if self.value >= self.thresholds.warn_at {
p.warning
} else {
p.brand_default
}
}
}
impl<'a> Widget for Gauge<'a> {
fn ui(self, ui: &mut Ui) -> Response {
let palette = palette_of(ui.ctx());
let fill = self.auto_color(&palette);
match self.shape {
GaugeShape::Ring => paint_ring(ui, &self, &palette, fill),
GaugeShape::Bar => paint_bar(ui, &self, &palette, fill),
}
}
}
fn paint_ring(ui: &mut Ui, g: &Gauge<'_>, p: &crate::Palette, fill: Color32) -> Response {
let d = g.size;
let (rect, response) = ui.allocate_exact_size(vec2(d, d), Sense::hover());
let center = rect.center();
let radius = d / 2.0 - 4.0;
let stroke_w = (d * 0.10).max(3.0);
let segments = 64;
let start = std::f32::consts::PI * 0.75; let end_full = std::f32::consts::PI * 2.25; let span = end_full - start;
let mut track = Vec::with_capacity(segments + 1);
for i in 0..=segments {
let a = start + (i as f32 / segments as f32) * span;
track.push(egui::pos2(
center.x + a.cos() * radius,
center.y + a.sin() * radius,
));
}
ui.painter().add(egui::Shape::line(
track,
Stroke::new(stroke_w, alpha(p.text_primary, 0.06)),
));
let fill_segments = (segments as f32 * g.value).round() as usize;
if fill_segments > 0 {
let mut arc = Vec::with_capacity(fill_segments + 1);
for i in 0..=fill_segments {
let a = start + (i as f32 / segments as f32) * span;
arc.push(egui::pos2(
center.x + a.cos() * radius,
center.y + a.sin() * radius,
));
}
ui.painter()
.add(egui::Shape::line(arc, Stroke::new(stroke_w, fill)));
}
if g.show_percent {
ui.painter().text(
center,
egui::Align2::CENTER_CENTER,
format!("{:.0}%", g.value * 100.0),
FontId::new(d * 0.28, egui::FontFamily::Proportional),
p.text_primary,
);
}
if let Some(label) = g.label {
ui.painter().text(
egui::pos2(center.x, rect.bottom() + 4.0),
egui::Align2::CENTER_TOP,
label,
FontId::new(11.0, egui::FontFamily::Proportional),
p.text_secondary,
);
}
response
}
fn paint_bar(ui: &mut Ui, g: &Gauge<'_>, p: &crate::Palette, fill: Color32) -> Response {
let width = ui.available_width().max(80.0);
let row_height = if g.label.is_some() || g.show_percent {
g.size + 18.0
} else {
g.size
};
let (rect, response) = ui.allocate_exact_size(vec2(width, row_height), Sense::hover());
if g.label.is_some() || g.show_percent {
let header_y = rect.top();
if let Some(label) = g.label {
ui.painter().text(
egui::pos2(rect.left(), header_y),
egui::Align2::LEFT_TOP,
label,
FontId::new(12.0, egui::FontFamily::Proportional),
p.text_secondary,
);
}
if g.show_percent {
ui.painter().text(
egui::pos2(rect.right(), header_y),
egui::Align2::RIGHT_TOP,
format!("{:.0}%", g.value * 100.0),
FontId::new(12.0, egui::FontFamily::Monospace),
p.text_secondary,
);
}
}
let bar_y = rect.bottom() - g.size;
let track = egui::Rect::from_min_size(egui::pos2(rect.left(), bar_y), vec2(width, g.size));
ui.painter().rect(
track,
corner(RADIUS.full),
p.bg_surface_alt,
Stroke::new(1.0, p.border_subtle),
StrokeKind::Inside,
);
let fill_w = width * g.value;
if fill_w > 0.0 {
let fill_rect = egui::Rect::from_min_size(track.min, vec2(fill_w, g.size));
ui.painter()
.rect_filled(fill_rect, corner(RADIUS.full), fill);
}
response
}