use crate::base::Point;
use crate::layout::{Dimension, Style as LayoutStyle};
use crate::theme::TokenSet;
use crate::ui::Element;
const EIGHTHS: [char; 7] = ['▏', '▎', '▍', '▌', '▋', '▊', '▉'];
pub struct Progress {
fraction: f32,
ramp: bool,
warn_at: f32,
error_at: f32,
layout: Option<LayoutStyle>,
}
impl Progress {
pub fn new(fraction: f32) -> Progress {
Progress {
fraction: fraction.clamp(0.0, 1.0),
ramp: false,
warn_at: 0.65,
error_at: 0.85,
layout: None,
}
}
pub fn ramp(mut self, on: bool) -> Progress {
self.ramp = on;
self
}
pub fn thresholds(mut self, warn_at: f32, error_at: f32) -> Progress {
self.warn_at = warn_at.clamp(0.0, 1.0);
self.error_at = error_at.clamp(0.0, 1.0).max(self.warn_at);
self
}
pub fn layout(mut self, style: LayoutStyle) -> Progress {
self.layout = Some(style);
self
}
pub fn element(self, t: &TokenSet) -> Element {
let fill = if self.ramp {
if self.fraction >= self.error_at {
t.error
} else if self.fraction >= self.warn_at {
t.warn
} else {
t.ok
}
} else {
t.accent
};
let track = t.surface_raised;
let fraction = self.fraction;
let layout = self
.layout
.unwrap_or_else(|| LayoutStyle::default().height(Dimension::Cells(1)).grow(1.0));
Element::new().style(layout).draw(move |canvas, rect| {
if rect.w <= 0 || rect.h <= 0 {
return;
}
let y = rect.y;
for x in rect.x..rect.right() {
canvas.put(Point::new(x, y), ' ', track, track);
}
let eighths_total = (fraction * (rect.w * 8) as f32).round() as i32;
let full = eighths_total / 8;
let part = eighths_total % 8;
for x in 0..full {
canvas.put(Point::new(rect.x + x, y), '█', fill, track);
}
if part > 0 && full < rect.w {
canvas.put(
Point::new(rect.x + full, y),
EIGHTHS[(part - 1) as usize],
fill,
track,
);
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::Size;
use crate::theme::default_theme;
use crate::widgets::test_util::{draw_into, row};
#[test]
fn sub_cell_precision_renders_partial_block() {
let t = default_theme().tokens;
let c = draw_into(Progress::new(0.5).element(&t), Size::new(10, 1));
assert_eq!(row(&c, 0), "█████ ");
let c = draw_into(Progress::new(0.56).element(&t), Size::new(10, 1));
assert_eq!(row(&c, 0), "█████▋ ");
}
#[test]
fn empty_and_full_are_exact() {
let t = default_theme().tokens;
let c = draw_into(Progress::new(0.0).element(&t), Size::new(8, 1));
assert_eq!(row(&c, 0), " ");
let c = draw_into(Progress::new(1.0).element(&t), Size::new(8, 1));
assert_eq!(row(&c, 0), "████████");
let c = draw_into(Progress::new(7.0).element(&t), Size::new(8, 1));
assert_eq!(row(&c, 0), "████████");
}
#[test]
fn ramp_colors_by_threshold() {
let t = default_theme().tokens;
let fill_color = |frac: f32| {
let c = draw_into(Progress::new(frac).ramp(true).element(&t), Size::new(8, 1));
c.cell(Point::new(0, 0)).unwrap().1
};
assert_eq!(fill_color(0.3), t.ok);
assert_eq!(fill_color(0.7), t.warn);
assert_eq!(fill_color(0.9), t.error);
let c = draw_into(Progress::new(0.5).element(&t), Size::new(8, 1));
assert_eq!(c.cell(Point::new(0, 0)).unwrap().1, t.accent);
}
#[test]
fn track_uses_raised_surface_and_zero_rect_is_safe() {
let t = default_theme().tokens;
let c = draw_into(Progress::new(0.2).element(&t), Size::new(8, 1));
assert_eq!(c.cell(Point::new(7, 0)).unwrap().2, t.surface_raised);
let _ = draw_into(Progress::new(0.2).element(&t), Size::new(0, 0));
}
}