use std::cmp;
use bon::Builder;
use widestring::{Utf16String, utf16str};
const BLOCKS: [char; 8] = [
'\u{258F}', '\u{258E}', '\u{258D}', '\u{258C}', '\u{258B}', '\u{258A}', '\u{2589}', '\u{2588}',
];
const BLOCKS_N: u64 = BLOCKS.len() as u64;
pub const HAIR_SPACE: char = '\u{200A}';
#[derive(Builder, Debug, Clone)]
pub struct StringBar {
value: u64,
max: u64,
#[builder(default = 15)]
width: u16,
#[builder(default)]
min_bar: bool,
#[builder(default)]
proportional_font: bool,
}
impl StringBar {
const fn width(&self) -> u64 {
self.width as _
}
fn min_bar(&self) -> Utf16String {
if self.min_bar {
utf16str!("\u{258F}").into()
} else {
Default::default()
}
}
pub fn to_utf16_string(&self) -> Utf16String {
if self.proportional_font {
self.proportional_font_to_utf16_string()
} else {
self.monospaced_font_to_utf16_string()
}
}
pub fn proportional_font_to_utf16_string(&self) -> Utf16String {
let i = self.min_bar as u64;
let n = if self.max == 0 {
i
} else {
cmp::min(
(self.value.saturating_mul(self.width()) / self.max) + i,
self.width(),
)
};
let bar = utf16str!("\u{258F}").repeat(n as usize);
bar
}
pub fn monospaced_font_to_utf16_string(&self) -> Utf16String {
if self.max == 0 {
return self.min_bar();
}
let units = self.value.saturating_mul(self.width()) / self.max;
if units == 0 {
return self.min_bar();
}
let full_blocks = units / BLOCKS_N;
let rem = units % BLOCKS_N;
let capacity = (full_blocks + ((rem > 0) as u64)) as usize;
let mut bar = Utf16String::with_capacity(capacity);
for _ in 0..full_blocks {
bar.push(*BLOCKS.last().unwrap());
}
if rem > 0 {
bar.push(BLOCKS[rem as usize - 1]);
}
bar
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty() {
let bar = StringBar::builder().value(0).max(100).width(80).build();
let result = bar.to_utf16_string();
assert!(result.is_empty());
let bar = StringBar::builder()
.value(0)
.max(100)
.width(80)
.min_bar(true)
.build();
let result = bar.to_utf16_string();
assert_eq!(result.to_string(), "\u{258F}");
}
#[test]
fn zero_max() {
let bar = StringBar::builder().value(5).max(0).width(80).build();
let result = bar.to_utf16_string();
assert!(result.is_empty());
let bar = StringBar::builder()
.value(5)
.max(0)
.width(80)
.min_bar(true)
.build();
let result = bar.to_utf16_string();
assert_eq!(result.to_string(), "\u{258F}");
}
#[test]
fn half_full() {
let bar = StringBar::builder().value(50).max(100).width(80).build();
let result = bar.to_utf16_string();
assert_eq!(result.len(), 5);
assert_eq!(result.to_string(), "█████");
}
#[test]
fn full() {
let bar = StringBar::builder().value(100).max(100).width(80).build();
let result = bar.to_utf16_string();
assert_eq!(result.len(), 10);
assert_eq!(result.to_string(), "██████████");
}
#[test]
fn partial_block_left() {
let bar = StringBar::builder().value(53).max(100).width(80).build();
let result = bar.to_utf16_string();
assert_eq!(result.len(), 6);
assert_eq!(result.to_string(), "█████▎");
assert_eq!(result.chars().nth(5).unwrap(), '\u{258E}');
}
}