use crate::color::Rgba;
use crate::error::Result;
use crate::framebuffer::Framebuffer;
use crate::geometry::Rect;
#[derive(Debug, Clone)]
pub struct ResourceBar {
label: String,
planned: f64,
actual: f64,
unit: String,
width: u32,
height: u32,
under_budget_color: Rgba,
over_budget_color: Rgba,
background_color: Rgba,
}
impl Default for ResourceBar {
fn default() -> Self {
Self {
label: String::new(),
planned: 1.0,
actual: 0.0,
unit: String::new(),
width: 200,
height: 20,
under_budget_color: Rgba::rgb(76, 175, 80), over_budget_color: Rgba::rgb(244, 67, 54), background_color: Rgba::rgb(224, 224, 224), }
}
}
impl ResourceBar {
#[must_use]
pub fn new(
label: impl Into<String>,
planned: f64,
actual: f64,
unit: impl Into<String>,
) -> Self {
Self {
label: label.into(),
planned: planned.max(0.0),
actual: actual.max(0.0),
unit: unit.into(),
..Self::default()
}
}
#[must_use]
pub fn dimensions(mut self, width: u32, height: u32) -> Self {
self.width = width.max(20);
self.height = height.max(8);
self
}
#[must_use]
pub fn under_budget_color(mut self, color: Rgba) -> Self {
self.under_budget_color = color;
self
}
#[must_use]
pub fn over_budget_color(mut self, color: Rgba) -> Self {
self.over_budget_color = color;
self
}
#[must_use]
pub fn background_color(mut self, color: Rgba) -> Self {
self.background_color = color;
self
}
#[must_use]
pub fn percentage(&self) -> f64 {
if self.planned <= 0.0 {
if self.actual <= 0.0 {
return 0.0;
}
return f64::INFINITY;
}
(self.actual / self.planned) * 100.0
}
#[must_use]
pub fn is_over_budget(&self) -> bool {
self.actual > self.planned
}
#[must_use]
pub fn label(&self) -> &str {
&self.label
}
#[must_use]
pub fn planned(&self) -> f64 {
self.planned
}
#[must_use]
pub fn actual(&self) -> f64 {
self.actual
}
#[must_use]
pub fn unit(&self) -> &str {
&self.unit
}
#[must_use]
pub fn fill_color(&self) -> Rgba {
if self.is_over_budget() {
self.over_budget_color
} else {
self.under_budget_color
}
}
pub fn render(&self, fb: &mut Framebuffer) -> Result<()> {
let bg_rect = Rect::new(0.0, 0.0, self.width as f32, self.height as f32);
fill_rect(fb, &bg_rect, self.background_color);
let max_value = self.planned.max(self.actual);
if max_value <= 0.0 {
return Ok(());
}
let actual_width = ((self.actual / max_value) * f64::from(self.width)) as f32;
let actual_width = actual_width.min(self.width as f32);
let fill_color = self.fill_color();
let actual_rect = Rect::new(0.0, 0.0, actual_width, self.height as f32);
fill_rect(fb, &actual_rect, fill_color);
if self.is_over_budget() {
let planned_x = ((self.planned / max_value) * f64::from(self.width)) as u32;
let planned_x = planned_x.min(self.width - 1);
for y in 0..self.height {
fb.set_pixel(planned_x, y, Rgba::BLACK);
}
}
Ok(())
}
pub fn to_framebuffer(&self) -> Result<Framebuffer> {
let mut fb = Framebuffer::new(self.width, self.height)?;
fb.clear(Rgba::TRANSPARENT);
self.render(&mut fb)?;
Ok(fb)
}
}
fn fill_rect(fb: &mut Framebuffer, rect: &Rect, color: Rgba) {
let x_start = rect.x.max(0.0) as u32;
let y_start = rect.y.max(0.0) as u32;
let x_end = (rect.x + rect.width).min(fb.width() as f32) as u32;
let y_end = (rect.y + rect.height).min(fb.height() as f32) as u32;
for y in y_start..y_end {
for x in x_start..x_end {
fb.set_pixel(x, y, color);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resource_bar_percentage() {
let bar = ResourceBar::new("GPU Hours", 100.0, 50.0, "hours");
assert!((bar.percentage() - 50.0).abs() < f64::EPSILON);
let bar = ResourceBar::new("GPU Hours", 100.0, 100.0, "hours");
assert!((bar.percentage() - 100.0).abs() < f64::EPSILON);
let bar = ResourceBar::new("GPU Hours", 100.0, 150.0, "hours");
assert!((bar.percentage() - 150.0).abs() < f64::EPSILON);
}
#[test]
fn test_resource_bar_percentage_edge_cases() {
let bar = ResourceBar::new("GPU Hours", 0.0, 0.0, "hours");
assert!((bar.percentage() - 0.0).abs() < f64::EPSILON);
let bar = ResourceBar::new("GPU Hours", 0.0, 50.0, "hours");
assert!(bar.percentage().is_infinite());
}
#[test]
fn test_resource_bar_over_budget() {
let under = ResourceBar::new("Time", 10.0, 5.0, "hours");
assert!(!under.is_over_budget());
let at = ResourceBar::new("Time", 10.0, 10.0, "hours");
assert!(!at.is_over_budget());
let over = ResourceBar::new("Time", 10.0, 15.0, "hours");
assert!(over.is_over_budget());
}
#[test]
fn test_resource_bar_render() {
let bar = ResourceBar::new("GPU Hours", 100.0, 75.0, "hours").dimensions(200, 20);
let fb = bar.to_framebuffer();
assert!(fb.is_ok());
let fb = fb.expect("operation should succeed");
assert_eq!(fb.width(), 200);
assert_eq!(fb.height(), 20);
}
#[test]
fn test_resource_bar_render_over_budget() {
let bar = ResourceBar::new("GPU Hours", 100.0, 150.0, "hours").dimensions(200, 20);
let fb = bar.to_framebuffer();
assert!(fb.is_ok());
}
#[test]
fn test_resource_bar_colors() {
let bar = ResourceBar::new("Test", 100.0, 50.0, "units");
assert_eq!(bar.fill_color(), bar.under_budget_color);
let bar = ResourceBar::new("Test", 100.0, 150.0, "units");
assert_eq!(bar.fill_color(), bar.over_budget_color);
}
#[test]
fn test_resource_bar_accessors() {
let bar = ResourceBar::new("GPU Hours", 100.0, 75.0, "hours");
assert_eq!(bar.label(), "GPU Hours");
assert!((bar.planned() - 100.0).abs() < f64::EPSILON);
assert!((bar.actual() - 75.0).abs() < f64::EPSILON);
assert_eq!(bar.unit(), "hours");
}
#[test]
fn test_resource_bar_default() {
let bar = ResourceBar::default();
assert_eq!(bar.label(), "");
assert_eq!(bar.unit(), "");
}
#[test]
fn test_resource_bar_clone_debug() {
let bar = ResourceBar::new("Test", 10.0, 5.0, "units");
let cloned = bar.clone();
let debug = format!("{cloned:?}");
assert!(debug.contains("ResourceBar"));
}
#[test]
fn test_resource_bar_custom_colors() {
let bar = ResourceBar::new("Test", 100.0, 50.0, "units")
.under_budget_color(Rgba::BLUE)
.over_budget_color(Rgba::RED)
.background_color(Rgba::WHITE);
let fb = bar.to_framebuffer();
assert!(fb.is_ok());
}
#[test]
fn test_resource_bar_dimensions_minimum() {
let bar = ResourceBar::new("Test", 10.0, 5.0, "units").dimensions(1, 1);
let fb = bar.to_framebuffer();
assert!(fb.is_ok());
}
}