use presentar_core::{
Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
};
use std::any::Any;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct BarSegment {
pub value: f64,
pub color: Color,
}
#[derive(Debug, Clone, Default)]
pub struct ProportionalBar {
pub segments: Vec<BarSegment>,
pub background_color: Option<Color>,
bounds: Rect,
}
impl ProportionalBar {
pub fn new() -> Self {
Self::default()
}
pub fn with_segment(mut self, value: f64, color: Color) -> Self {
self.segments.push(BarSegment { value, color });
self
}
pub fn with_background(mut self, color: Color) -> Self {
self.background_color = Some(color);
self
}
pub fn total_value(&self) -> f64 {
self.segments.iter().map(|s| s.value).sum()
}
fn get_block_char(fraction: f64) -> (char, bool) {
if fraction >= 1.0 {
('█', true)
} else if fraction >= 0.875 {
('▇', false)
} else if fraction >= 0.75 {
('▆', false)
} else if fraction >= 0.625 {
('▅', false)
} else if fraction >= 0.5 {
('▄', false)
} else if fraction >= 0.375 {
('▃', false)
} else if fraction >= 0.25 {
('▂', false)
} else if fraction >= 0.125 {
('▁', false)
} else {
(' ', false) }
}
}
impl Widget for ProportionalBar {
fn type_id(&self) -> TypeId {
TypeId::of::<Self>()
}
fn measure(&self, constraints: Constraints) -> Size {
constraints.constrain(Size::new(constraints.max_width, 1.0))
}
fn layout(&mut self, bounds: Rect) -> LayoutResult {
self.bounds = bounds;
LayoutResult {
size: Size::new(bounds.width, bounds.height),
}
}
fn paint(&self, canvas: &mut dyn Canvas) {
if self.bounds.width < 1.0 || self.bounds.height < 1.0 {
return;
}
let width_chars = self.bounds.width as usize;
let x = self.bounds.x;
let y = self.bounds.y;
let total = self.total_value();
let _safe_total = if total.is_nan() { 0.0 } else { total.min(1.0) };
if let Some(bg) = self.background_color {
canvas.fill_rect(Rect::new(x, y, self.bounds.width, 1.0), bg);
}
let mut current_pos_chars = 0.0;
for segment in &self.segments {
let val = if segment.value.is_nan() {
0.0
} else {
segment.value
};
if val <= 0.0 {
continue;
}
let segment_width_chars = val * width_chars as f64;
let end_pos_chars = current_pos_chars + segment_width_chars;
let start_idx = current_pos_chars.floor() as usize;
let end_idx = end_pos_chars.floor() as usize;
for i in start_idx..end_idx {
if i < width_chars {
canvas.draw_text(
"█",
Point::new(x + i as f32, y),
&TextStyle {
color: segment.color,
..Default::default()
},
);
}
}
let fractional_part = end_pos_chars - end_pos_chars.floor();
if fractional_part > 0.001 && end_idx < width_chars {
let (ch, _) = Self::get_block_char(fractional_part);
canvas.draw_text(
&ch.to_string(),
Point::new(x + end_idx as f32, y),
&TextStyle {
color: segment.color,
..Default::default()
},
);
}
current_pos_chars = end_pos_chars;
}
}
fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
None
}
fn children(&self) -> &[Box<dyn Widget>] {
&[]
}
fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
&mut []
}
}
impl Brick for ProportionalBar {
fn brick_name(&self) -> &'static str {
"proportional_bar"
}
fn assertions(&self) -> &[BrickAssertion] {
static ASSERTIONS: &[BrickAssertion] = &[
BrickAssertion::max_latency_ms(1), ];
ASSERTIONS
}
fn budget(&self) -> BrickBudget {
BrickBudget::uniform(1) }
fn verify(&self) -> BrickVerification {
let total = self.total_value();
let nan_safe = !total.is_nan(); let bounds_safe = total <= 1.0 + f64::EPSILON;
if nan_safe && bounds_safe {
BrickVerification {
passed: self.assertions().to_vec(),
failed: vec![],
verification_time: Duration::from_micros(1),
}
} else {
BrickVerification {
passed: vec![],
failed: self
.assertions()
.iter()
.map(|a| (a.clone(), "NaN or bounds violation".to_string()))
.collect(),
verification_time: Duration::from_micros(1),
}
}
}
fn to_html(&self) -> String {
let mut html =
String::from("<div class=\"proportional-bar\" style=\"display:flex;height:1em;\">");
let total: f64 = self.segments.iter().map(|s| s.value).sum();
for seg in &self.segments {
let pct = if total > 0.0 {
(seg.value / total) * 100.0
} else {
0.0
};
let Color { r, g, b, .. } = seg.color;
html.push_str(&format!(
"<div style=\"width:{pct:.1}%;background:rgb({r},{g},{b})\"></div>"
));
}
html.push_str("</div>");
html
}
fn to_css(&self) -> String {
String::from(".proportional-bar{display:flex;height:1em;width:100%}.proportional-bar>div{min-width:1px}")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::direct::{CellBuffer, DirectTerminalCanvas};
#[test]
fn test_f_atom_001_no_bleed() {
let mut bar = ProportionalBar::new()
.with_segment(0.6, Color::RED)
.with_segment(0.6, Color::BLUE);
let mut buffer = CellBuffer::new(10, 1);
let mut canvas = DirectTerminalCanvas::new(&mut buffer);
bar.layout(Rect::new(0.0, 0.0, 10.0, 1.0));
bar.paint(&mut canvas);
assert!(true, "Implementation limits loop to width_chars");
}
#[test]
fn test_f_atom_002_nan_safe() {
let bar = ProportionalBar::new().with_segment(f64::NAN, Color::RED);
let _v = bar.verify();
let mut buffer = CellBuffer::new(10, 1);
let mut canvas = DirectTerminalCanvas::new(&mut buffer);
bar.paint(&mut canvas); }
#[test]
fn test_f_atom_003_linear_interpolation() {
let (ch, _) = ProportionalBar::get_block_char(0.5);
assert_eq!(ch, '▄');
let (ch, _) = ProportionalBar::get_block_char(0.1);
assert_eq!(ch, ' ');
let (ch, _) = ProportionalBar::get_block_char(0.9);
assert_eq!(ch, '▇'); }
#[test]
fn test_bar_segment_debug() {
let seg = BarSegment {
value: 0.5,
color: Color::RED,
};
let debug = format!("{:?}", seg);
assert!(debug.contains("BarSegment"));
}
#[test]
fn test_bar_segment_clone() {
let seg = BarSegment {
value: 0.75,
color: Color::BLUE,
};
let cloned = seg.clone();
assert!((cloned.value - 0.75).abs() < f64::EPSILON);
}
#[test]
fn test_proportional_bar_default() {
let bar = ProportionalBar::default();
assert!(bar.segments.is_empty());
assert!(bar.background_color.is_none());
}
#[test]
fn test_proportional_bar_new() {
let bar = ProportionalBar::new();
assert!(bar.segments.is_empty());
}
#[test]
fn test_proportional_bar_debug() {
let bar = ProportionalBar::new();
let debug = format!("{:?}", bar);
assert!(debug.contains("ProportionalBar"));
}
#[test]
fn test_proportional_bar_clone() {
let bar = ProportionalBar::new()
.with_segment(0.3, Color::RED)
.with_background(Color::BLACK);
let cloned = bar.clone();
assert_eq!(cloned.segments.len(), 1);
assert!(cloned.background_color.is_some());
}
#[test]
fn test_with_segment() {
let bar = ProportionalBar::new()
.with_segment(0.25, Color::RED)
.with_segment(0.35, Color::GREEN);
assert_eq!(bar.segments.len(), 2);
}
#[test]
fn test_with_background() {
let bar = ProportionalBar::new().with_background(Color::rgb(0.5, 0.5, 0.5));
assert!(bar.background_color.is_some());
}
#[test]
fn test_total_value() {
let bar = ProportionalBar::new()
.with_segment(0.2, Color::RED)
.with_segment(0.3, Color::GREEN)
.with_segment(0.1, Color::BLUE);
let total = bar.total_value();
assert!((total - 0.6).abs() < f64::EPSILON);
}
#[test]
fn test_total_value_empty() {
let bar = ProportionalBar::new();
assert!((bar.total_value() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_get_block_char_full() {
let (ch, full) = ProportionalBar::get_block_char(1.0);
assert_eq!(ch, '█');
assert!(full);
}
#[test]
fn test_get_block_char_above_full() {
let (ch, full) = ProportionalBar::get_block_char(1.5);
assert_eq!(ch, '█');
assert!(full);
}
#[test]
fn test_get_block_char_875() {
let (ch, full) = ProportionalBar::get_block_char(0.88);
assert_eq!(ch, '▇');
assert!(!full);
}
#[test]
fn test_get_block_char_75() {
let (ch, _) = ProportionalBar::get_block_char(0.76);
assert_eq!(ch, '▆');
}
#[test]
fn test_get_block_char_625() {
let (ch, _) = ProportionalBar::get_block_char(0.63);
assert_eq!(ch, '▅');
}
#[test]
fn test_get_block_char_375() {
let (ch, _) = ProportionalBar::get_block_char(0.38);
assert_eq!(ch, '▃');
}
#[test]
fn test_get_block_char_25() {
let (ch, _) = ProportionalBar::get_block_char(0.26);
assert_eq!(ch, '▂');
}
#[test]
fn test_get_block_char_125() {
let (ch, _) = ProportionalBar::get_block_char(0.13);
assert_eq!(ch, '▁');
}
#[test]
fn test_get_block_char_zero() {
let (ch, full) = ProportionalBar::get_block_char(0.0);
assert_eq!(ch, ' ');
assert!(!full);
}
#[test]
fn test_get_block_char_negative() {
let (ch, _) = ProportionalBar::get_block_char(-0.5);
assert_eq!(ch, ' ');
}
#[test]
fn test_measure() {
let bar = ProportionalBar::new();
let size = bar.measure(Constraints {
min_width: 0.0,
min_height: 0.0,
max_width: 50.0,
max_height: 10.0,
});
assert!((size.width - 50.0).abs() < f32::EPSILON);
assert!((size.height - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_layout() {
let mut bar = ProportionalBar::new();
let result = bar.layout(Rect::new(5.0, 10.0, 20.0, 1.0));
assert!((result.size.width - 20.0).abs() < f32::EPSILON);
assert!((result.size.height - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_paint_empty_bar() {
let bar = ProportionalBar::new();
let mut buffer = CellBuffer::new(10, 1);
let mut canvas = DirectTerminalCanvas::new(&mut buffer);
bar.paint(&mut canvas);
}
#[test]
fn test_paint_zero_width() {
let mut bar = ProportionalBar::new().with_segment(0.5, Color::RED);
bar.layout(Rect::new(0.0, 0.0, 0.0, 1.0));
let mut buffer = CellBuffer::new(0, 1);
let mut canvas = DirectTerminalCanvas::new(&mut buffer);
bar.paint(&mut canvas);
}
#[test]
fn test_paint_zero_height() {
let mut bar = ProportionalBar::new().with_segment(0.5, Color::RED);
bar.layout(Rect::new(0.0, 0.0, 10.0, 0.0));
let mut buffer = CellBuffer::new(10, 0);
let mut canvas = DirectTerminalCanvas::new(&mut buffer);
bar.paint(&mut canvas);
}
#[test]
fn test_paint_with_background() {
let mut bar = ProportionalBar::new()
.with_segment(0.3, Color::RED)
.with_background(Color::rgb(0.5, 0.5, 0.5));
bar.layout(Rect::new(0.0, 0.0, 10.0, 1.0));
let mut buffer = CellBuffer::new(10, 1);
let mut canvas = DirectTerminalCanvas::new(&mut buffer);
bar.paint(&mut canvas);
}
#[test]
fn test_paint_multiple_segments() {
let mut bar = ProportionalBar::new()
.with_segment(0.3, Color::RED)
.with_segment(0.3, Color::GREEN)
.with_segment(0.3, Color::BLUE);
bar.layout(Rect::new(0.0, 0.0, 20.0, 1.0));
let mut buffer = CellBuffer::new(20, 1);
let mut canvas = DirectTerminalCanvas::new(&mut buffer);
bar.paint(&mut canvas);
}
#[test]
fn test_paint_zero_value_segment() {
let mut bar = ProportionalBar::new()
.with_segment(0.0, Color::RED) .with_segment(0.5, Color::GREEN);
bar.layout(Rect::new(0.0, 0.0, 10.0, 1.0));
let mut buffer = CellBuffer::new(10, 1);
let mut canvas = DirectTerminalCanvas::new(&mut buffer);
bar.paint(&mut canvas);
}
#[test]
fn test_paint_negative_value_segment() {
let mut bar = ProportionalBar::new()
.with_segment(-0.5, Color::RED) .with_segment(0.5, Color::GREEN);
bar.layout(Rect::new(0.0, 0.0, 10.0, 1.0));
let mut buffer = CellBuffer::new(10, 1);
let mut canvas = DirectTerminalCanvas::new(&mut buffer);
bar.paint(&mut canvas);
}
#[test]
fn test_paint_fractional_segments() {
let mut bar = ProportionalBar::new().with_segment(0.15, Color::RED); bar.layout(Rect::new(0.0, 0.0, 10.0, 1.0));
let mut buffer = CellBuffer::new(10, 1);
let mut canvas = DirectTerminalCanvas::new(&mut buffer);
bar.paint(&mut canvas);
}
#[test]
fn test_type_id() {
let bar = ProportionalBar::new();
let _ = Widget::type_id(&bar);
}
#[test]
fn test_event() {
let mut bar = ProportionalBar::new();
let result = bar.event(&Event::Resize {
width: 100.0,
height: 50.0,
});
assert!(result.is_none());
}
#[test]
fn test_children() {
let bar = ProportionalBar::new();
assert!(bar.children().is_empty());
}
#[test]
fn test_children_mut() {
let mut bar = ProportionalBar::new();
assert!(bar.children_mut().is_empty());
}
#[test]
fn test_brick_name() {
let bar = ProportionalBar::new();
assert_eq!(bar.brick_name(), "proportional_bar");
}
#[test]
fn test_assertions() {
let bar = ProportionalBar::new();
let assertions = bar.assertions();
assert!(!assertions.is_empty());
}
#[test]
fn test_budget() {
let bar = ProportionalBar::new();
let _budget = bar.budget();
}
#[test]
fn test_verify_valid() {
let bar = ProportionalBar::new()
.with_segment(0.3, Color::RED)
.with_segment(0.4, Color::GREEN);
let verification = bar.verify();
assert!(verification.failed.is_empty());
}
#[test]
fn test_verify_exceeds_bounds() {
let bar = ProportionalBar::new()
.with_segment(0.6, Color::RED)
.with_segment(0.6, Color::GREEN); let verification = bar.verify();
assert!(!verification.failed.is_empty());
}
#[test]
fn test_verify_nan() {
let bar = ProportionalBar::new().with_segment(f64::NAN, Color::RED);
let verification = bar.verify();
assert!(!verification.failed.is_empty());
}
#[test]
fn test_to_html() {
let bar = ProportionalBar::new();
let html = bar.to_html();
assert!(html.contains("proportional-bar"));
assert!(html.starts_with("<div"));
assert!(html.ends_with("</div>"));
}
#[test]
fn test_to_css() {
let bar = ProportionalBar::new();
let css = bar.to_css();
assert!(css.contains("proportional-bar"));
assert!(css.contains("display:flex"));
}
}