pub mod easing;
use crate::{BrailleGrid, Color, DotmaxError};
pub use easing::{ease, lerp, Easing};
#[derive(Debug, Clone)]
pub struct BarContext {
pub progress: f32,
pub eased: f32,
pub time: f32,
pub width: usize,
pub height: usize,
pub palette: Palette,
pub label: Option<String>,
}
impl BarContext {
#[must_use]
pub fn new(progress: f32, time: f32, width: usize, height: usize) -> Self {
let progress = progress.clamp(0.0, 1.0);
Self {
progress,
eased: progress,
time,
width,
height,
palette: Palette::default(),
label: None,
}
}
#[must_use]
pub fn with_easing(mut self, kind: Easing) -> Self {
self.eased = ease(kind, self.progress);
self
}
#[must_use]
pub const fn with_palette(mut self, palette: Palette) -> Self {
self.palette = palette;
self
}
#[must_use]
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
}
#[derive(Debug, Clone, Copy)]
pub struct Palette {
pub start: Color,
pub end: Color,
pub track: Color,
}
impl Palette {
#[must_use]
pub fn sample(&self, t: f32) -> Color {
let t = t.clamp(0.0, 1.0);
Color::rgb(
lerp(f32::from(self.start.r), f32::from(self.end.r), t) as u8,
lerp(f32::from(self.start.g), f32::from(self.end.g), t) as u8,
lerp(f32::from(self.start.b), f32::from(self.end.b), t) as u8,
)
}
}
impl Default for Palette {
fn default() -> Self {
Self {
start: Color::rgb(0, 200, 255),
end: Color::rgb(120, 80, 255),
track: Color::rgb(40, 40, 50),
}
}
}
pub trait ProgressStyle {
fn name(&self) -> &str;
fn theme(&self) -> &str;
fn describe(&self) -> &str {
"a loading bar"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError>;
}
pub mod draw {
use crate::BrailleGrid;
#[must_use]
pub fn dot_dims(grid: &BrailleGrid) -> (usize, usize) {
let (w, h) = grid.dimensions();
(w * 2, h * 4)
}
pub fn dot(grid: &mut BrailleGrid, x: usize, y: usize) {
let (w, h) = dot_dims(grid);
if x < w && y < h {
let _ = grid.set_dot(x, y);
}
}
pub fn dot_i(grid: &mut BrailleGrid, x: i32, y: i32) {
if x >= 0 && y >= 0 {
dot(grid, x as usize, y as usize);
}
}
pub fn hline(grid: &mut BrailleGrid, x0: usize, x1: usize, y: usize) {
let (lo, hi) = if x0 <= x1 { (x0, x1) } else { (x1, x0) };
for x in lo..=hi {
dot(grid, x, y);
}
}
pub fn vline(grid: &mut BrailleGrid, x: usize, y0: usize, y1: usize) {
let (lo, hi) = if y0 <= y1 { (y0, y1) } else { (y1, y0) };
for y in lo..=hi {
dot(grid, x, y);
}
}
pub fn fill_rect(grid: &mut BrailleGrid, x0: usize, y0: usize, w: usize, h: usize) {
for y in y0..y0 + h {
for x in x0..x0 + w {
dot(grid, x, y);
}
}
}
pub fn rect_outline(grid: &mut BrailleGrid, x0: usize, y0: usize, w: usize, h: usize) {
if w == 0 || h == 0 {
return;
}
let (x1, y1) = (x0 + w - 1, y0 + h - 1);
hline(grid, x0, x1, y0);
hline(grid, x0, x1, y1);
vline(grid, x0, y0, y1);
vline(grid, x1, y0, y1);
}
pub fn tint_row(
grid: &mut BrailleGrid,
cell_y: usize,
cell_x0: usize,
cell_x1: usize,
color: crate::Color,
) {
grid.enable_color_support();
let (w, h) = grid.dimensions();
if cell_y >= h {
return;
}
let hi = cell_x1.min(w.saturating_sub(1));
for x in cell_x0..=hi {
let _ = grid.set_cell_color(x, cell_y, color);
}
}
pub const H_BLOCKS: [char; 9] = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
pub const V_BLOCKS: [char; 9] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
pub const SHADES: [char; 5] = [' ', '░', '▒', '▓', '█'];
pub fn glyph(grid: &mut BrailleGrid, cell_x: usize, cell_y: usize, c: char) {
let _ = grid.set_char(cell_x, cell_y, c);
}
pub fn hbar(grid: &mut BrailleGrid, cell_y: usize, frac: f32) {
let (w, _) = grid.dimensions();
let frac = frac.clamp(0.0, 1.0);
let eighths = (frac * (w * 8) as f32).round() as usize;
let full = eighths / 8;
let rem = eighths % 8;
for x in 0..full.min(w) {
glyph(grid, x, cell_y, '█');
}
if rem > 0 && full < w {
glyph(grid, full, cell_y, H_BLOCKS[rem]);
}
}
pub fn vblock(grid: &mut BrailleGrid, cell_x: usize, cell_y: usize, level: usize) {
glyph(grid, cell_x, cell_y, V_BLOCKS[level.min(8)]);
}
pub fn shade(grid: &mut BrailleGrid, cell_x: usize, cell_y: usize, level: usize) {
glyph(grid, cell_x, cell_y, SHADES[level.min(4)]);
}
}
pub fn render_lines(
style: &dyn ProgressStyle,
ctx: &BarContext,
) -> Result<Vec<String>, DotmaxError> {
let mut grid = BrailleGrid::new(ctx.width.max(1), ctx.height.max(1))?;
style.render(&mut grid, ctx)?;
let (w, h) = grid.dimensions();
let mut lines = Vec::with_capacity(h);
for y in 0..h {
let mut row = String::with_capacity(w);
for x in 0..w {
row.push(grid.get_char(x, y));
}
lines.push(row);
}
Ok(lines)
}
pub fn render_string(style: &dyn ProgressStyle, ctx: &BarContext) -> Result<String, DotmaxError> {
Ok(render_lines(style, ctx)?.join("\n"))
}
mod styles;
pub use styles::{all_styles, styles_for_theme, themes};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_styles_render_robustly() {
let sizes = [(1, 1), (2, 1), (50, 4), (80, 1), (10, 8)];
let progresses = [0.0, 0.001, 0.5, 0.999, 1.0];
let times = [0.0, 1.5, 3.7, 100.0];
for style in all_styles() {
assert!(!style.name().is_empty(), "style has empty name");
assert!(
!style.theme().is_empty(),
"{} has empty theme",
style.name()
);
for &(w, h) in &sizes {
for &p in &progresses {
for &t in × {
let mut grid = BrailleGrid::new(w, h).unwrap();
let ctx = BarContext::new(p, t, w, h)
.with_easing(Easing::CubicInOut)
.with_palette(Palette::default());
style.render(&mut grid, &ctx).unwrap_or_else(|e| {
panic!("{} failed at {w}x{h} p={p} t={t}: {e}", style.name())
});
}
}
}
}
}
#[test]
fn names_unique_per_theme() {
use std::collections::HashSet;
for theme in themes() {
let mut seen = HashSet::new();
for style in styles_for_theme(theme) {
assert!(
seen.insert(style.name().to_string()),
"duplicate name '{}' in theme '{}'",
style.name(),
theme
);
}
}
}
}