use ratatui::buffer::Buffer;
use ratatui::layout::{Position, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::widgets::{Block, Borders, Paragraph};
use crate::component::Component;
use crate::component::context::{EventContext, RenderContext};
use crate::input::Event;
use crate::theme::Theme;
mod values;
pub use values::ResourceValues;
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub enum GaugeOrientation {
#[default]
Horizontal,
Vertical,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct ResourceGaugeState {
actual: f64,
request: f64,
limit: f64,
label: Option<String>,
units: Option<String>,
title: Option<String>,
show_legend: bool,
orientation: GaugeOrientation,
disabled: bool,
}
impl Default for ResourceGaugeState {
fn default() -> Self {
Self {
actual: 0.0,
request: 0.0,
limit: 0.0,
label: None,
units: None,
title: None,
show_legend: true,
orientation: GaugeOrientation::default(),
disabled: false,
}
}
}
impl ResourceGaugeState {
pub fn with_values(mut self, values: ResourceValues) -> Self {
self.actual = values.actual;
self.request = values.request;
self.limit = values.limit;
self
}
pub fn with_actual(mut self, actual: f64) -> Self {
self.actual = actual;
self
}
pub fn with_request(mut self, request: f64) -> Self {
self.request = request;
self
}
pub fn with_limit(mut self, limit: f64) -> Self {
self.limit = limit;
self
}
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_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_show_legend(mut self, show: bool) -> Self {
self.show_legend = show;
self
}
pub fn with_orientation(mut self, orientation: GaugeOrientation) -> Self {
self.orientation = orientation;
self
}
pub fn with_disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn actual(&self) -> f64 {
self.actual
}
pub fn request(&self) -> f64 {
self.request
}
pub fn limit(&self) -> f64 {
self.limit
}
pub fn label(&self) -> Option<&str> {
self.label.as_deref()
}
pub fn units(&self) -> Option<&str> {
self.units.as_deref()
}
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
pub fn show_legend(&self) -> bool {
self.show_legend
}
pub fn orientation(&self) -> &GaugeOrientation {
&self.orientation
}
pub fn is_disabled(&self) -> bool {
self.disabled
}
pub fn utilization(&self) -> f64 {
if self.limit <= 0.0 {
0.0
} else {
(self.actual / self.limit).clamp(0.0, 1.0)
}
}
pub fn request_ratio(&self) -> f64 {
if self.request <= 0.0 {
0.0
} else {
self.actual / self.request
}
}
pub fn is_over_request(&self) -> bool {
self.actual >= self.request && self.request > 0.0
}
pub fn is_near_limit(&self) -> bool {
self.limit > 0.0 && self.actual >= self.limit * 0.9
}
pub fn set_actual(&mut self, actual: f64) {
self.actual = actual;
}
pub fn set_request(&mut self, request: f64) {
self.request = request;
}
pub fn set_limit(&mut self, limit: f64) {
self.limit = limit;
}
pub fn values(&self) -> ResourceValues {
ResourceValues {
actual: self.actual,
request: self.request,
limit: self.limit,
}
}
pub fn set_values(&mut self, actual: f64, request: f64, limit: f64) {
self.actual = actual;
self.request = request;
self.limit = limit;
}
pub fn set_label(&mut self, label: Option<String>) {
self.label = label;
}
pub fn set_units(&mut self, units: Option<String>) {
self.units = units;
}
pub fn set_title(&mut self, title: Option<String>) {
self.title = title;
}
pub fn update(&mut self, msg: ResourceGaugeMessage) -> Option<()> {
ResourceGauge::update(self, msg)
}
fn health_color(&self) -> Color {
if self.limit <= 0.0 {
Color::DarkGray
} else if self.actual >= self.limit * 0.9 {
Color::Red
} else if self.request > 0.0 && self.actual >= self.request {
Color::Yellow
} else {
Color::Green
}
}
fn legend_text(&self) -> String {
let units = self.units.as_deref().unwrap_or("");
format!(
"{}{} / {}{} / {}{}",
format_value(self.actual),
units,
format_value(self.request),
units,
format_value(self.limit),
units,
)
}
}
fn format_value(v: f64) -> String {
if v == v.floor() {
format!("{}", v as i64)
} else {
format!("{:.1}", v)
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum ResourceGaugeMessage {
SetActual(f64),
SetRequest(f64),
SetLimit(f64),
SetValues {
actual: f64,
request: f64,
limit: f64,
},
SetLabel(Option<String>),
SetUnits(Option<String>),
}
pub type ResourceGaugeOutput = ();
pub struct ResourceGauge;
impl Component for ResourceGauge {
type State = ResourceGaugeState;
type Message = ResourceGaugeMessage;
type Output = ResourceGaugeOutput;
fn init() -> Self::State {
ResourceGaugeState::default()
}
fn handle_event(
_state: &Self::State,
_event: &Event,
_ctx: &EventContext,
) -> Option<Self::Message> {
None }
fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
match msg {
ResourceGaugeMessage::SetActual(v) => state.actual = v,
ResourceGaugeMessage::SetRequest(v) => state.request = v,
ResourceGaugeMessage::SetLimit(v) => state.limit = v,
ResourceGaugeMessage::SetValues {
actual,
request,
limit,
} => {
state.actual = actual;
state.request = request;
state.limit = limit;
}
ResourceGaugeMessage::SetLabel(l) => state.label = l,
ResourceGaugeMessage::SetUnits(u) => state.units = u,
}
None
}
fn view(state: &Self::State, ctx: &mut RenderContext<'_, '_>) {
let disabled = ctx.disabled || state.disabled;
let border_style = if disabled {
ctx.theme.disabled_style()
} else if ctx.focused {
ctx.theme.focused_border_style()
} else {
ctx.theme.border_style()
};
let mut block = Block::default()
.borders(Borders::ALL)
.border_style(border_style);
if let Some(title) = &state.title {
block = block.title(format!(" {} ", title));
}
let inner = block.inner(ctx.area);
ctx.frame.render_widget(block, ctx.area);
if inner.width < 3 || inner.height < 1 {
return;
}
let label_width = state
.label
.as_ref()
.map(|l| l.len() as u16 + 1)
.unwrap_or(0);
let legend = if state.show_legend {
state.legend_text()
} else {
String::new()
};
let legend_width = if legend.is_empty() {
0
} else {
legend.len() as u16 + 1
};
let bar_width = inner
.width
.saturating_sub(label_width)
.saturating_sub(legend_width);
if bar_width < 2 {
let text = if let Some(label) = &state.label {
format!("{} {}", label, legend)
} else {
legend
};
let style = if disabled {
ctx.theme.disabled_style()
} else {
Style::default().fg(state.health_color())
};
ctx.frame
.render_widget(Paragraph::new(text).style(style), inner);
return;
}
let bar_y = inner.y;
let buf = ctx.frame.buffer_mut();
let buf_area = buf.area;
if let Some(label) = &state.label {
let label_style = if disabled {
ctx.theme.disabled_style()
} else {
Style::default().add_modifier(Modifier::BOLD)
};
for (i, ch) in label.chars().enumerate() {
let x = inner.x + i as u16;
if x < inner.x + label_width {
set_cell(buf, x, bar_y, &ch.to_string(), label_style, buf_area);
}
}
}
let bar_x = inner.x + label_width;
let bar_params = BarParams {
state,
disabled,
theme: ctx.theme,
};
render_bar(buf, bar_x, bar_y, bar_width, &bar_params);
if !legend.is_empty() {
let legend_x = bar_x + bar_width + 1;
let legend_style = if disabled {
ctx.theme.disabled_style()
} else {
Style::default().fg(state.health_color())
};
for (i, ch) in legend.chars().enumerate() {
let x = legend_x + i as u16;
if x < inner.x + inner.width {
set_cell(buf, x, bar_y, &ch.to_string(), legend_style, buf_area);
}
}
}
}
}
struct BarParams<'a> {
state: &'a ResourceGaugeState,
disabled: bool,
theme: &'a Theme,
}
fn render_bar(buf: &mut Buffer, x: u16, y: u16, width: u16, params: &BarParams<'_>) {
let state = params.state;
let disabled = params.disabled;
let buf_area = buf.area;
if state.limit <= 0.0 || width == 0 {
for i in 0..width {
set_cell(
buf,
x + i,
y,
"\u{2591}",
Style::default().fg(Color::DarkGray),
buf_area,
);
}
return;
}
let actual_ratio = (state.actual / state.limit).clamp(0.0, 1.0);
let request_ratio = if state.request > 0.0 {
(state.request / state.limit).clamp(0.0, 1.0)
} else {
0.0
};
let actual_pos = ((actual_ratio * width as f64) as u16).min(width);
let request_pos = ((request_ratio * width as f64) as u16).min(width);
let fill_color = if disabled {
params.theme.disabled_style().fg.unwrap_or(Color::DarkGray)
} else {
state.health_color()
};
let fill_style = Style::default().fg(fill_color);
let request_zone_style = Style::default().fg(Color::DarkGray);
let empty_style = Style::default().fg(Color::DarkGray);
for i in 0..width {
let cx = x + i;
if i < actual_pos {
set_cell(buf, cx, y, "\u{2588}", fill_style, buf_area);
} else if i < request_pos {
set_cell(buf, cx, y, "\u{2593}", request_zone_style, buf_area);
} else {
set_cell(buf, cx, y, "\u{2591}", empty_style, buf_area);
}
}
if request_pos > 0 && request_pos < width && state.request > 0.0 {
let marker_style = if disabled {
Style::default().fg(Color::DarkGray)
} else {
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD)
};
set_cell(buf, x + request_pos, y, "\u{2502}", marker_style, buf_area);
}
}
fn set_cell(buf: &mut Buffer, x: u16, y: u16, ch: &str, style: Style, area: Rect) {
if x >= area.x && x < area.right() && y >= area.y && y < area.bottom() {
if let Some(cell) = buf.cell_mut(Position::new(x, y)) {
cell.set_symbol(ch);
cell.set_style(style);
}
}
}
#[cfg(test)]
mod tests;