use std::f32::consts::PI;
use egui::{
epaint::{PathShape, PathStroke},
pos2, Color32, CornerRadius, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Ui, Vec2, Widget,
WidgetInfo, WidgetType,
};
use crate::theme::{placeholder_galley, with_alpha, Palette, Theme, BASELINE_FRAC};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GaugeZones {
warn: f32,
crit: f32,
}
impl GaugeZones {
pub fn new(warn: f32, crit: f32) -> Self {
let warn = warn.clamp(0.0, 1.0);
let crit = crit.clamp(0.0, 1.0).max(warn);
Self { warn, crit }
}
pub const fn warn(self) -> f32 {
self.warn
}
pub const fn crit(self) -> f32 {
self.crit
}
pub(crate) fn color(&self, fraction: f32, palette: &Palette) -> Color32 {
if fraction >= self.crit {
palette.danger
} else if fraction >= self.warn {
palette.warning
} else {
palette.success
}
}
}
fn clamp_fraction(f: f32) -> f32 {
if f.is_nan() {
0.0
} else {
f.clamp(0.0, 1.0)
}
}
fn track_color(palette: &Palette) -> Color32 {
if palette.is_dark {
palette.bg
} else {
palette.depth_tint(palette.input_bg, 0.04)
}
}
#[derive(Clone, Debug)]
#[must_use = "Add with `ui.add(...)`."]
pub struct RadialGauge {
fraction: f32,
size: f32,
color: Option<Color32>,
zones: Option<GaugeZones>,
needle: bool,
text: Option<String>,
unit: Option<String>,
show_scale: bool,
}
impl RadialGauge {
pub fn new(fraction: f32) -> Self {
Self {
fraction: clamp_fraction(fraction),
size: 200.0,
color: None,
zones: None,
needle: true,
text: None,
unit: None,
show_scale: true,
}
}
#[inline]
pub fn size(mut self, size: f32) -> Self {
self.size = size.max(80.0);
self
}
#[inline]
pub fn color(mut self, color: Color32) -> Self {
self.color = Some(color);
self
}
#[inline]
pub fn zones(mut self, zones: GaugeZones) -> Self {
self.zones = Some(zones);
self
}
#[inline]
pub fn needle(mut self, on: bool) -> Self {
self.needle = on;
self
}
#[inline]
pub fn text(mut self, text: impl Into<String>) -> Self {
self.text = Some(text.into());
self
}
#[inline]
pub fn unit(mut self, unit: impl Into<String>) -> Self {
self.unit = Some(unit.into());
self
}
#[inline]
pub fn show_scale(mut self, on: bool) -> Self {
self.show_scale = on;
self
}
}
impl Widget for RadialGauge {
fn ui(self, ui: &mut Ui) -> Response {
let theme = Theme::current(ui.ctx());
let p = &theme.palette;
let scale_size = (self.size * 0.052).clamp(9.0, 12.0);
let scale_h = if self.show_scale {
scale_size + 4.0
} else {
0.0
};
let arc_h = self.size * 0.74;
let total_h = arc_h + scale_h;
let (rect, response) =
ui.allocate_exact_size(Vec2::new(self.size, total_h), Sense::hover());
if ui.is_rect_visible(rect) {
let painter = ui.painter();
let arc_rect = Rect::from_min_size(rect.min, Vec2::new(self.size, arc_h));
let cx = arc_rect.center().x;
let cy = arc_rect.top() + self.size * 0.5;
let r = self.size * 0.4;
let stroke_w = self.size * 0.07;
let n_segments: usize = 96;
let arc_point = |t: f32| -> Pos2 {
let a = PI - PI * t;
pos2(cx + r * a.cos(), cy - r * a.sin())
};
let arc_points = |start: f32, end: f32| -> Vec<Pos2> {
let span = (end - start).max(0.0);
let n = ((n_segments as f32 * span).ceil() as usize).max(2);
(0..=n)
.map(|i| arc_point(start + span * (i as f32 / n as f32)))
.collect()
};
painter.add(PathShape::line(
arc_points(0.0, 1.0),
PathStroke::new(stroke_w, track_color(p)),
));
if let Some(z) = &self.zones {
painter.add(PathShape::line(
arc_points(0.0, z.warn),
PathStroke::new(stroke_w, with_alpha(p.success, 56)),
));
painter.add(PathShape::line(
arc_points(z.warn, z.crit),
PathStroke::new(stroke_w, with_alpha(p.warning, 60)),
));
painter.add(PathShape::line(
arc_points(z.crit, 1.0),
PathStroke::new(stroke_w, with_alpha(p.danger, 66)),
));
}
let fill_color = self.color.unwrap_or_else(|| {
self.zones
.as_ref()
.map(|z| z.color(self.fraction, p))
.unwrap_or(p.sky)
});
if self.fraction > 0.0 {
painter.add(PathShape::line(
arc_points(0.0, self.fraction),
PathStroke::new(stroke_w, fill_color),
));
}
if let Some(z) = &self.zones {
for &boundary in &[z.warn, z.crit] {
let a = PI - PI * boundary;
let inner_r = r + stroke_w * 0.5 + 1.0;
let outer_r = inner_r + stroke_w * 0.55;
let inner = pos2(cx + inner_r * a.cos(), cy - inner_r * a.sin());
let outer = pos2(cx + outer_r * a.cos(), cy - outer_r * a.sin());
painter.line_segment([inner, outer], Stroke::new(1.0, p.text_muted));
}
}
if self.needle {
let a = PI - PI * self.fraction;
let needle_len = r * 0.9;
let half_w = (self.size * 0.013).max(1.5);
let perp = a + PI * 0.5;
let tip = pos2(cx + needle_len * a.cos(), cy - needle_len * a.sin());
let base_l = pos2(cx + half_w * perp.cos(), cy - half_w * perp.sin());
let base_r = pos2(cx - half_w * perp.cos(), cy + half_w * perp.sin());
painter.add(PathShape::convex_polygon(
vec![tip, base_l, base_r],
p.text,
Stroke::NONE,
));
let pivot_r = (self.size * 0.03).max(4.0);
painter.circle_filled(pos2(cx, cy), pivot_r, p.card);
painter.circle_stroke(pos2(cx, cy), pivot_r, Stroke::new(1.5, p.text));
painter.circle_filled(pos2(cx, cy), pivot_r * 0.28, p.bg);
}
let primary_size = (self.size * 0.15).clamp(14.0, 36.0);
let unit_size = (self.size * 0.085).clamp(12.0, 22.0);
let primary = self
.text
.clone()
.unwrap_or_else(|| format!("{}", (self.fraction * 100.0).round() as u32));
let unit = self.unit.clone().unwrap_or_else(|| {
if self.text.is_none() {
"%".into()
} else {
String::new()
}
});
if !primary.is_empty() {
let g_num = placeholder_galley(ui, &primary, primary_size, true, f32::INFINITY);
let g_unit = (!unit.is_empty())
.then(|| placeholder_galley(ui, &unit, unit_size, false, f32::INFINITY));
let num_w = g_num.size().x;
let num_h = g_num.size().y;
let unit_w = g_unit.as_ref().map_or(0.0, |g| g.size().x);
let gap = if g_unit.is_some() { 3.0 } else { 0.0 };
let total_w = num_w + gap + unit_w;
let bottom_y = arc_rect.bottom() - 6.0;
let num_top = bottom_y - num_h;
let start_x = cx - total_w * 0.5;
painter.galley(pos2(start_x, num_top), g_num, p.text);
if let Some(g) = g_unit {
let baseline = num_top + num_h * BASELINE_FRAC;
let unit_y = baseline - g.size().y * BASELINE_FRAC;
painter.galley(pos2(start_x + num_w + gap, unit_y), g, p.text_muted);
}
}
if self.show_scale {
let label_y = arc_rect.bottom() + 2.0;
let g_left = placeholder_galley(ui, "0", scale_size, false, f32::INFINITY);
let g_right = placeholder_galley(ui, "100", scale_size, false, f32::INFINITY);
painter.galley(
pos2(cx - r - g_left.size().x * 0.5, label_y),
g_left,
p.text_faint,
);
painter.galley(
pos2(cx + r - g_right.size().x * 0.5, label_y),
g_right,
p.text_faint,
);
}
}
response.widget_info(|| WidgetInfo::labeled(WidgetType::ProgressIndicator, true, "gauge"));
response
}
}
#[derive(Clone, Debug)]
#[must_use = "Add with `ui.add(...)`."]
pub struct LinearGauge {
fraction: f32,
height: f32,
desired_width: Option<f32>,
color: Option<Color32>,
zones: Option<GaugeZones>,
threshold_labels: Vec<(f32, String)>,
thumb: bool,
}
impl LinearGauge {
pub fn new(fraction: f32) -> Self {
Self {
fraction: clamp_fraction(fraction),
height: 14.0,
desired_width: None,
color: None,
zones: None,
threshold_labels: Vec::new(),
thumb: true,
}
}
#[inline]
pub fn height(mut self, height: f32) -> Self {
self.height = height.max(6.0);
self
}
#[inline]
pub fn desired_width(mut self, width: f32) -> Self {
self.desired_width = Some(width);
self
}
#[inline]
pub fn color(mut self, color: Color32) -> Self {
self.color = Some(color);
self
}
#[inline]
pub fn zones(mut self, zones: GaugeZones) -> Self {
self.zones = Some(zones);
self
}
pub fn threshold_label(mut self, position: f32, label: impl Into<String>) -> Self {
self.threshold_labels
.push((position.clamp(0.0, 1.0), label.into()));
self
}
pub fn show_zone_labels(mut self) -> Self {
if let Some(z) = self.zones {
self.threshold_labels
.push((z.warn, format!("{}", (z.warn * 100.0).round() as u32)));
self.threshold_labels
.push((z.crit, format!("{}", (z.crit * 100.0).round() as u32)));
}
self
}
#[inline]
pub fn thumb(mut self, on: bool) -> Self {
self.thumb = on;
self
}
}
impl Widget for LinearGauge {
fn ui(self, ui: &mut Ui) -> Response {
let theme = Theme::current(ui.ctx());
let p = &theme.palette;
let label_size = 10.0;
let label_pad = 6.0;
let label_h = if self.threshold_labels.is_empty() {
0.0
} else {
label_size + label_pad
};
let width = self
.desired_width
.unwrap_or_else(|| ui.available_width())
.max(self.height * 4.0);
let total_h = self.height + label_h;
let (rect, response) = ui.allocate_exact_size(Vec2::new(width, total_h), Sense::hover());
if ui.is_rect_visible(rect) {
let painter = ui.painter();
let bar_rect = Rect::from_min_size(
pos2(rect.left(), rect.top() + label_h),
Vec2::new(width, self.height),
);
let radius = CornerRadius::same((self.height * 0.5).round() as u8);
painter.rect(
bar_rect,
radius,
p.input_bg,
Stroke::new(1.0, p.border),
StrokeKind::Inside,
);
if let Some(z) = &self.zones {
let r = (self.height * 0.5).round() as u8;
let band =
|start: f32, end: f32, color: Color32, left_round: bool, right_round: bool| {
if end <= start {
return;
}
let x0 = bar_rect.left() + bar_rect.width() * start;
let x1 = bar_rect.left() + bar_rect.width() * end;
let cr = CornerRadius {
nw: if left_round { r } else { 0 },
sw: if left_round { r } else { 0 },
ne: if right_round { r } else { 0 },
se: if right_round { r } else { 0 },
};
let rect = Rect::from_min_max(
pos2(x0, bar_rect.top()),
pos2(x1, bar_rect.bottom()),
);
painter.rect_filled(rect.shrink(0.5), cr, color);
};
band(0.0, z.warn, with_alpha(p.success, 50), true, false);
band(z.warn, z.crit, with_alpha(p.warning, 56), false, false);
band(z.crit, 1.0, with_alpha(p.danger, 60), false, true);
}
let fill_color = self.color.unwrap_or_else(|| {
self.zones
.as_ref()
.map(|z| z.color(self.fraction, p))
.unwrap_or(p.sky)
});
let fill_w = bar_rect.width() * self.fraction;
if fill_w > 0.5 {
let fill_rect =
Rect::from_min_size(bar_rect.min, Vec2::new(fill_w, bar_rect.height()));
painter
.with_clip_rect(fill_rect)
.rect_filled(bar_rect, radius, fill_color);
}
if self.thumb && self.fraction > 0.0 {
let x = bar_rect.left() + fill_w;
painter.line_segment(
[
pos2(x, bar_rect.top() + 1.0),
pos2(x, bar_rect.bottom() - 1.0),
],
Stroke::new(2.0, p.text),
);
}
for (pos, label) in &self.threshold_labels {
let x = bar_rect.left() + bar_rect.width() * pos.clamp(0.0, 1.0);
let g = placeholder_galley(ui, label, label_size, false, f32::INFINITY);
let label_y = rect.top();
painter.galley(pos2(x - g.size().x * 0.5, label_y), g, p.text_faint);
let tick_top = label_y + label_size + 1.0;
let tick_bot = bar_rect.top() - 1.0;
if tick_bot > tick_top {
painter.line_segment(
[pos2(x, tick_top), pos2(x, tick_bot)],
Stroke::new(1.0, p.text_faint),
);
}
}
}
response.widget_info(|| WidgetInfo::labeled(WidgetType::ProgressIndicator, true, "meter"));
response
}
}