use crate::render::{Cell, Modifier};
use crate::style::Color;
use crate::utils::color::contrast_color;
use crate::utils::{char_width, display_width};
use crate::widget::theme::SEPARATOR_COLOR;
use crate::widget::traits::{RenderContext, View, WidgetProps};
use crate::{impl_props_builders, impl_styled_view};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum GaugeStyle {
#[default]
Bar,
Battery,
Thermometer,
Arc,
Circle,
Vertical,
Segments,
Dots,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum LabelPosition {
None,
#[default]
Inside,
Left,
Right,
Above,
Below,
}
pub struct Gauge {
value: f64,
min: f64,
max: f64,
style: GaugeStyle,
width: u16,
height: u16,
label: Option<String>,
label_position: LabelPosition,
show_percent: bool,
fill_color: Color,
fill_bg: Option<Color>,
empty_color: Color,
empty_bg: Option<Color>,
border_color: Option<Color>,
warning_threshold: Option<f64>,
critical_threshold: Option<f64>,
warning_color: Color,
critical_color: Color,
segments: u16,
title: Option<String>,
props: WidgetProps,
}
impl Gauge {
pub fn new() -> Self {
Self {
value: 0.0,
min: 0.0,
max: 100.0,
style: GaugeStyle::Bar,
width: 20,
height: 5,
label: None,
label_position: LabelPosition::Inside,
show_percent: true,
fill_color: Color::GREEN,
fill_bg: None,
empty_color: SEPARATOR_COLOR,
empty_bg: None,
border_color: None,
warning_threshold: None,
critical_threshold: None,
warning_color: Color::YELLOW,
critical_color: Color::RED,
segments: 10,
title: None,
props: WidgetProps::new(),
}
}
pub fn value(mut self, value: f64) -> Self {
self.value = value.clamp(0.0, 1.0);
self
}
pub fn value_range(mut self, value: f64, min: f64, max: f64) -> Self {
let (min, max) = if min < max { (min, max) } else { (max, min) };
self.min = min;
self.max = max;
let range = max - min;
if range.abs() < f64::EPSILON {
self.value = 0.0;
} else {
self.value = ((value - min) / range).clamp(0.0, 1.0);
}
self
}
pub fn percent(mut self, percent: f64) -> Self {
self.value = (percent / 100.0).clamp(0.0, 1.0);
self
}
pub fn style(mut self, style: GaugeStyle) -> Self {
self.style = style;
self
}
pub fn width(mut self, width: u16) -> Self {
self.width = width.max(4);
self
}
pub fn height(mut self, height: u16) -> Self {
self.height = height.max(1);
self
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn label_position(mut self, position: LabelPosition) -> Self {
self.label_position = position;
self
}
pub fn show_percent(mut self, show: bool) -> Self {
self.show_percent = show;
self
}
pub fn fill_color(mut self, color: Color) -> Self {
self.fill_color = color;
self
}
pub fn fill_background(mut self, color: Color) -> Self {
self.fill_bg = Some(color);
self
}
pub fn empty_color(mut self, color: Color) -> Self {
self.empty_color = color;
self
}
pub fn empty_background(mut self, color: Color) -> Self {
self.empty_bg = Some(color);
self
}
pub fn border(mut self, color: Color) -> Self {
self.border_color = Some(color);
self
}
pub fn thresholds(mut self, warning: f64, critical: f64) -> Self {
let warning = warning.clamp(0.0, 1.0);
let critical = critical.clamp(0.0, 1.0);
let (warning, critical) = if warning < critical {
(warning, critical)
} else {
(critical, warning)
};
self.warning_threshold = Some(warning);
self.critical_threshold = Some(critical);
self
}
pub fn warning_color(mut self, color: Color) -> Self {
self.warning_color = color;
self
}
pub fn critical_color(mut self, color: Color) -> Self {
self.critical_color = color;
self
}
pub fn segments(mut self, count: u16) -> Self {
self.segments = count.max(2);
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
fn current_color(&self) -> Color {
if let Some(critical) = self.critical_threshold {
if self.value >= critical {
return self.critical_color;
}
}
if let Some(warning) = self.warning_threshold {
if self.value >= warning {
return self.warning_color;
}
}
self.fill_color
}
fn get_label(&self) -> String {
if let Some(ref label) = self.label {
label.clone()
} else if self.show_percent {
format!("{:.0}%", self.value * 100.0)
} else {
let display_value = self.min + self.value * (self.max - self.min);
format!("{:.0}", display_value)
}
}
pub fn set_value(&mut self, value: f64) {
self.value = value.clamp(0.0, 1.0);
}
pub fn get_value(&self) -> f64 {
self.value
}
fn render_bar(&self, ctx: &mut RenderContext) {
let area = ctx.area;
let width = self.width.min(area.width);
let filled = (self.value * width as f64).round() as u16;
let color = self.current_color();
for x in 0..width {
let is_filled = x < filled;
let ch = if is_filled { '█' } else { '░' };
let fg = if is_filled { color } else { self.empty_color };
let bg = if is_filled {
self.fill_bg.unwrap_or(color)
} else {
self.empty_bg.unwrap_or(self.empty_color)
};
let mut cell = Cell::new(ch);
cell.fg = Some(fg);
cell.bg = Some(bg);
ctx.set(x, 0, cell);
}
if matches!(self.label_position, LabelPosition::Inside) {
let label = self.get_label();
let lw = display_width(&label) as u16;
let label_x = (width.saturating_sub(lw)) / 2;
let mut dx: u16 = 0;
for ch in label.chars() {
let cw = char_width(ch) as u16;
let x = label_x + dx;
if x + cw <= width {
let is_filled = x < filled;
let bg = if is_filled {
self.fill_bg.unwrap_or(color)
} else {
self.empty_bg.unwrap_or(self.empty_color)
};
let mut cell = Cell::new(ch);
cell.fg = Some(contrast_color(bg));
cell.bg = Some(bg);
cell.modifier |= Modifier::BOLD;
ctx.set(x, 0, cell);
}
dx += cw;
}
}
}
fn render_battery(&self, ctx: &mut RenderContext) {
let area = ctx.area;
let width = self.width.min(area.width).max(6);
let inner_width = width - 3; let filled = (self.value * inner_width as f64).round() as u16;
let color = self.current_color();
let mut left = Cell::new('[');
left.fg = Some(Color::WHITE);
ctx.set(0, 0, left);
for x in 0..inner_width {
let ch = if x < filled { '█' } else { ' ' };
let fg = if x < filled { color } else { self.empty_color };
let mut cell = Cell::new(ch);
cell.fg = Some(fg);
ctx.set(1 + x, 0, cell);
}
let mut right = Cell::new(']');
right.fg = Some(Color::WHITE);
ctx.set(1 + inner_width, 0, right);
let mut cap = Cell::new('▌');
cap.fg = Some(Color::WHITE);
ctx.set(2 + inner_width, 0, cap);
}
fn render_thermometer(&self, ctx: &mut RenderContext) {
let area = ctx.area;
let height = self.height.min(area.height).max(3);
let filled = (self.value * (height - 1) as f64).round() as u16;
let color = self.current_color();
let mut bulb = Cell::new('●');
bulb.fg = Some(color);
ctx.set(0, height - 1, bulb);
for y in 0..height - 1 {
let from_bottom = height - 2 - y;
let ch = if from_bottom < filled { '█' } else { '│' };
let fg = if from_bottom < filled {
color
} else {
self.empty_color
};
let mut cell = Cell::new(ch);
cell.fg = Some(fg);
ctx.set(0, y, cell);
}
}
fn render_arc(&self, ctx: &mut RenderContext) {
let area = ctx.area;
let color = self.current_color();
let width = self.width.min(area.width).max(8);
let mut tl = Cell::new('╭');
tl.fg = Some(color);
ctx.set(0, 0, tl);
for x in 1..width - 1 {
let progress = (x - 1) as f64 / (width - 3) as f64;
let ch = if progress <= self.value { '━' } else { '─' };
let fg = if progress <= self.value {
color
} else {
self.empty_color
};
let mut cell = Cell::new(ch);
cell.fg = Some(fg);
ctx.set(x, 0, cell);
}
let mut tr = Cell::new('╮');
tr.fg = Some(color);
ctx.set(width - 1, 0, tr);
if area.height > 1 {
let label = self.get_label();
let lw = display_width(&label) as u16;
let label_x = (width.saturating_sub(lw)) / 2;
let mut left = Cell::new('│');
left.fg = Some(color);
ctx.set(0, 1, left);
let mut dx: u16 = 0;
for ch in label.chars() {
let cw = char_width(ch) as u16;
let mut cell = Cell::new(ch);
cell.fg = Some(contrast_color(self.empty_bg.unwrap_or(Color::rgb(0, 0, 0))));
cell.modifier |= Modifier::BOLD;
ctx.set(label_x + dx, 1, cell);
dx += cw;
}
let mut right = Cell::new('│');
right.fg = Some(color);
ctx.set(width - 1, 1, right);
}
if area.height > 2 {
let mut bl = Cell::new('╰');
bl.fg = Some(color);
ctx.set(0, 2, bl);
for x in 1..width - 1 {
let mut cell = Cell::new('─');
cell.fg = Some(self.empty_color);
ctx.set(x, 2, cell);
}
let mut br = Cell::new('╯');
br.fg = Some(color);
ctx.set(width - 1, 2, br);
}
}
fn render_circle(&self, ctx: &mut RenderContext) {
let color = self.current_color();
let label = self.get_label();
let segments = 5u16;
let filled = (self.value * segments as f64).round() as u16;
let mut open = Cell::new('(');
open.fg = Some(Color::WHITE);
ctx.set(0, 0, open);
for i in 0..segments {
let ch = if i < filled { '●' } else { '○' };
let fg = if i < filled { color } else { self.empty_color };
let mut cell = Cell::new(ch);
cell.fg = Some(fg);
ctx.set(1 + i, 0, cell);
}
let mut close = Cell::new(')');
close.fg = Some(Color::WHITE);
ctx.set(1 + segments, 0, close);
let label_x = 3 + segments;
let mut dx: u16 = 0;
for ch in label.chars() {
let cw = char_width(ch) as u16;
let mut cell = Cell::new(ch);
cell.fg = Some(contrast_color(self.empty_bg.unwrap_or(Color::rgb(0, 0, 0))));
ctx.set(label_x + dx, 0, cell);
dx += cw;
}
}
fn render_vertical(&self, ctx: &mut RenderContext) {
let area = ctx.area;
let height = self.height.min(area.height);
let filled = (self.value * height as f64).round() as u16;
let color = self.current_color();
for y in 0..height {
let from_bottom = height - 1 - y;
let ch = if from_bottom < filled { '█' } else { '░' };
let fg = if from_bottom < filled {
color
} else {
self.empty_color
};
let mut cell = Cell::new(ch);
cell.fg = Some(fg);
ctx.set(0, y, cell);
}
}
fn render_segments(&self, ctx: &mut RenderContext) {
let area = ctx.area;
let segments = self.segments.min(area.width / 2);
let filled = (self.value * segments as f64).round() as u16;
let color = self.current_color();
for i in 0..segments {
let ch = if i < filled { '▰' } else { '▱' };
let fg = if i < filled { color } else { self.empty_color };
let mut cell = Cell::new(ch);
cell.fg = Some(fg);
ctx.set(i * 2, 0, cell);
}
}
fn render_dots(&self, ctx: &mut RenderContext) {
let area = ctx.area;
let dots = self.segments.min(area.width);
let filled = (self.value * dots as f64).round() as u16;
let color = self.current_color();
for i in 0..dots {
let ch = if i < filled { '●' } else { '○' };
let fg = if i < filled { color } else { self.empty_color };
let mut cell = Cell::new(ch);
cell.fg = Some(fg);
ctx.set(i, 0, cell);
}
}
}
impl Default for Gauge {
fn default() -> Self {
Self::new()
}
}
impl View for Gauge {
crate::impl_view_meta!("Gauge");
fn render(&self, ctx: &mut RenderContext) {
let area = ctx.area;
if area.width == 0 || area.height == 0 {
return;
}
let mut y_offset = 0u16;
if let Some(ref title) = self.title {
for (i, ch) in title.chars().enumerate() {
if i as u16 >= area.width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(Color::WHITE);
cell.modifier |= Modifier::BOLD;
ctx.set(i as u16, 0, cell);
}
y_offset = 1;
}
let adjusted_area = ctx.sub_area(
0,
y_offset,
area.width,
area.height.saturating_sub(y_offset),
);
let mut adjusted_ctx = RenderContext::new(ctx.buffer, adjusted_area);
match self.style {
GaugeStyle::Bar => self.render_bar(&mut adjusted_ctx),
GaugeStyle::Battery => self.render_battery(&mut adjusted_ctx),
GaugeStyle::Thermometer => self.render_thermometer(&mut adjusted_ctx),
GaugeStyle::Arc => self.render_arc(&mut adjusted_ctx),
GaugeStyle::Circle => self.render_circle(&mut adjusted_ctx),
GaugeStyle::Vertical => self.render_vertical(&mut adjusted_ctx),
GaugeStyle::Segments => self.render_segments(&mut adjusted_ctx),
GaugeStyle::Dots => self.render_dots(&mut adjusted_ctx),
}
}
}
impl_styled_view!(Gauge);
impl_props_builders!(Gauge);
pub fn gauge() -> Gauge {
Gauge::new()
}
pub fn percentage(value: f64) -> Gauge {
Gauge::new().percent(value)
}
pub fn battery(level: f64) -> Gauge {
Gauge::new()
.percent(level)
.style(GaugeStyle::Battery)
.thresholds(0.5, 0.2)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::Rect;
use crate::render::Buffer;
#[test]
fn test_gauge_new() {
let g = Gauge::new();
assert_eq!(g.value, 0.0);
}
#[test]
fn test_gauge_value() {
let g = Gauge::new().value(0.75);
assert_eq!(g.value, 0.75);
}
#[test]
fn test_gauge_value_clamped() {
let g = Gauge::new().value(1.5);
assert_eq!(g.value, 1.0);
let g = Gauge::new().value(-0.5);
assert_eq!(g.value, 0.0);
}
#[test]
fn test_gauge_percent() {
let g = Gauge::new().percent(50.0);
assert!((g.value - 0.5).abs() < 0.01);
}
#[test]
fn test_gauge_styles() {
let _ = Gauge::new().style(GaugeStyle::Bar);
let _ = Gauge::new().style(GaugeStyle::Battery);
let _ = Gauge::new().style(GaugeStyle::Arc);
let _ = Gauge::new().style(GaugeStyle::Vertical);
}
#[test]
fn test_gauge_render_no_panic() {
let mut buf = Buffer::new(20, 3);
let area = Rect::new(0, 0, 20, 3);
let mut ctx = RenderContext::new(&mut buf, area);
let g = Gauge::new().value(0.5).style(GaugeStyle::Bar);
g.render(&mut ctx);
}
#[test]
fn test_gauge_helpers() {
let g = gauge().value(0.7);
assert_eq!(g.value, 0.7);
let b = battery(80.0);
assert!((b.value - 0.8).abs() < 0.01);
}
}