use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Gauge as RatatuiGauge, LineGauge};
use super::{Component, EventContext, RenderContext};
use crate::theme::Theme;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub enum GaugeVariant {
#[default]
Full,
Line,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct ThresholdZone {
pub above: f64,
pub color: Color,
}
#[derive(Clone, Debug, PartialEq)]
pub enum GaugeMessage {
SetValue(f64),
SetMax(f64),
SetLabel(Option<String>),
SetUnits(Option<String>),
}
pub type GaugeOutput = ();
fn default_thresholds() -> Vec<ThresholdZone> {
vec![
ThresholdZone {
above: 0.0,
color: Color::Green,
},
ThresholdZone {
above: 0.7,
color: Color::Yellow,
},
ThresholdZone {
above: 0.9,
color: Color::Red,
},
]
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct GaugeState {
value: f64,
max: f64,
label: Option<String>,
units: Option<String>,
variant: GaugeVariant,
thresholds: Vec<ThresholdZone>,
title: Option<String>,
}
impl Default for GaugeState {
fn default() -> Self {
Self {
value: 0.0,
max: 100.0,
label: None,
units: None,
variant: GaugeVariant::default(),
thresholds: default_thresholds(),
title: None,
}
}
}
impl GaugeState {
pub fn new(value: f64, max: f64) -> Self {
Self {
value,
max,
..Self::default()
}
}
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn with_units(mut self, units: impl Into<String>) -> Self {
self.units = Some(units.into());
self
}
pub fn with_variant(mut self, variant: GaugeVariant) -> Self {
self.variant = variant;
self
}
pub fn with_thresholds(mut self, mut thresholds: Vec<ThresholdZone>) -> Self {
thresholds.sort_by(|a, b| {
a.above
.partial_cmp(&b.above)
.unwrap_or(std::cmp::Ordering::Equal)
});
self.thresholds = thresholds;
self
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
pub fn set_title(&mut self, title: impl Into<String>) {
self.title = Some(title.into());
}
pub fn value(&self) -> f64 {
self.value
}
pub fn max(&self) -> f64 {
self.max
}
pub fn set_value(&mut self, value: f64) {
self.value = value;
}
pub fn set_max(&mut self, max: f64) {
self.max = max;
}
pub fn percentage(&self) -> f64 {
if self.max <= 0.0 {
return 0.0;
}
(self.value / self.max).clamp(0.0, 1.0)
}
pub fn display_percentage(&self) -> u16 {
(self.percentage() * 100.0) as u16
}
pub fn current_color(&self) -> Color {
let pct = self.percentage();
let mut color = Color::Green;
for zone in &self.thresholds {
if pct >= zone.above {
color = zone.color;
} else {
break;
}
}
color
}
pub fn label_text(&self) -> String {
if let Some(label) = &self.label {
return label.clone();
}
if let Some(units) = &self.units {
format!("{:.1} / {:.1} {}", self.value, self.max, units)
} else {
format!("{}%", self.display_percentage())
}
}
pub fn update(&mut self, msg: GaugeMessage) -> Option<GaugeOutput> {
Gauge::update(self, msg)
}
pub fn handle_event(&self, event: &crate::input::Event) -> Option<GaugeMessage> {
Gauge::handle_event(self, event, &EventContext::default())
}
pub fn dispatch_event(&mut self, event: &crate::input::Event) -> Option<GaugeOutput> {
Gauge::dispatch_event(self, event, &EventContext::default())
}
}
pub struct Gauge;
impl Component for Gauge {
type State = GaugeState;
type Message = GaugeMessage;
type Output = GaugeOutput;
fn init() -> Self::State {
GaugeState::default()
}
fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
match msg {
GaugeMessage::SetValue(value) => {
state.value = value;
}
GaugeMessage::SetMax(max) => {
state.max = max;
}
GaugeMessage::SetLabel(label) => {
state.label = label;
}
GaugeMessage::SetUnits(units) => {
state.units = units;
}
}
None
}
fn view(state: &Self::State, ctx: &mut RenderContext<'_, '_>) {
let label_text = state.label_text();
match state.variant {
GaugeVariant::Full => {
render_full_gauge(
state,
ctx.frame,
ctx.area,
ctx.theme,
&label_text,
ctx.disabled,
);
}
GaugeVariant::Line => {
render_line_gauge(
state,
ctx.frame,
ctx.area,
ctx.theme,
&label_text,
ctx.disabled,
);
}
}
}
}
fn render_full_gauge(
state: &GaugeState,
frame: &mut Frame,
area: Rect,
theme: &Theme,
label_text: &str,
disabled: bool,
) {
let color = if disabled {
theme.disabled
} else {
state.current_color()
};
let block = build_block(state, theme);
let gauge = RatatuiGauge::default()
.block(block)
.percent(state.display_percentage())
.label(label_text.to_string())
.gauge_style(Style::default().fg(color).bg(theme.background));
let annotation =
crate::annotation::Annotation::new(crate::annotation::WidgetType::Custom("Gauge".into()))
.with_id("gauge")
.with_label(label_text)
.with_value(format!("{}%", state.display_percentage()));
let annotated = crate::annotation::Annotate::new(gauge, annotation);
frame.render_widget(annotated, area);
}
fn render_line_gauge(
state: &GaugeState,
frame: &mut Frame,
area: Rect,
theme: &Theme,
label_text: &str,
disabled: bool,
) {
let color = if disabled {
theme.disabled
} else {
state.current_color()
};
let mut gauge = LineGauge::default()
.ratio(state.percentage())
.label(label_text.to_string())
.filled_style(Style::default().fg(color))
.unfilled_style(theme.disabled_style());
if let Some(title) = &state.title {
let block = Block::default()
.borders(Borders::ALL)
.border_style(theme.border_style())
.title(title.clone());
gauge = gauge.block(block);
}
let annotation =
crate::annotation::Annotation::new(crate::annotation::WidgetType::Custom("Gauge".into()))
.with_id("gauge")
.with_label(label_text)
.with_value(format!("{}%", state.display_percentage()));
let annotated = crate::annotation::Annotate::new(gauge, annotation);
frame.render_widget(annotated, area);
}
fn build_block(state: &GaugeState, theme: &Theme) -> Block<'static> {
let mut block = Block::default()
.borders(Borders::ALL)
.border_style(theme.border_style());
if let Some(title) = &state.title {
block = block.title(title.clone());
}
block
}
#[cfg(test)]
mod tests;