kkx/kkdisp/src/token.rs

265 lines
8.0 KiB
Rust

use crate::{component::Component, theme::Color};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Token {
Centered(Box<Token>),
Limited(Box<Token>, usize),
CharPad(Box<Token>, char, usize),
Fg(Box<Token>, Color),
Bg(Box<Token>, Color),
String(Box<Token>, String),
End,
}
impl From<String> for Token {
fn from(value: String) -> Self {
Self::text(value)
}
}
impl From<&str> for Token {
fn from(value: &str) -> Self {
Self::text(value)
}
}
impl Token {
pub fn text<T>(t: T) -> Self
where
T: Into<String>,
{
Self::End.string(t)
}
pub fn string<S>(self, s: S) -> Self
where
S: Into<String>,
{
Token::String(Box::new(self), s.into())
}
pub fn centered(self) -> Self {
Self::Centered(Box::new(self))
}
pub fn limited(self, lim: usize) -> Self {
Self::Limited(Box::new(self), lim)
}
pub fn padded(self, pad_to: usize) -> Self {
Self::CharPad(Box::new(self), ' ', pad_to)
}
pub fn str_len(&self) -> usize {
match self {
Token::String(t, s) => s.len() + t.str_len(),
Token::Fg(t, _) => t.str_len(),
Token::Bg(t, _) => t.str_len(),
Token::Centered(t) => t.str_len(),
Token::Limited(t, lim) => (*lim).min(t.str_len()),
Token::CharPad(t, _, pad_to) => {
(*pad_to).max(t.str_len())
}
Token::End => 0,
}
}
pub fn pad_char(self, c: char, pad_to: usize) -> Self {
Self::CharPad(Box::new(self), c, pad_to)
}
pub fn fg(self, c: Color) -> Self {
Self::Fg(Box::new(self), c)
}
pub fn bg(self, c: Color) -> Self {
Self::Bg(Box::new(self), c)
}
fn with_width(self, width: usize) -> Vec<Component> {
match self {
Token::String(t, s) => vec![Component::String(s)]
.into_iter()
.chain(t.with_width(width))
.collect(),
Token::Centered(cnt) => {
let mut str_len = cnt.str_len();
let components = if str_len > width {
str_len = width;
cnt.limited(width).with_width(width)
} else {
cnt.with_width(width)
};
vec![Component::X((width - str_len) / 2)]
.into_iter()
.chain(components)
.collect()
}
Token::Limited(s, lim) => s
.with_width(width)
.into_iter()
.map(|cmp| {
if let Component::String(mut s) = cmp {
s.truncate(lim);
Component::String(s)
} else {
cmp
}
})
.collect(),
Token::CharPad(s, c, pad_to) => s
.with_width(width)
.into_iter()
.map(|comp| {
if let Component::String(s) = comp {
if s.len() >= pad_to {
return Component::String(s);
}
Component::String(format!(
"{}{}",
s,
c.to_string().repeat(pad_to - s.len())
))
} else {
comp
}
})
.collect(),
Token::Fg(t, c) => vec![Component::Fg(c)]
.into_iter()
.chain(t.with_width(width))
.collect(),
Token::Bg(t, c) => vec![Component::Bg(c)]
.into_iter()
.chain(t.with_width(width))
.collect(),
Token::End => vec![],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_token_gen() {
const WIDTH: usize = 20;
vec![
(
"string -> string",
Token::text("hello"),
vec![Component::String("hello".into())],
),
(
"string -> limited",
Token::text("This string is too long for this!")
.limited(4),
vec![Component::String("This".into())],
),
(
"string -> limit -> pad_to",
Token::text("This string is too long, but some")
.limited(10)
.padded(15),
vec![Component::String("This strin ".into())],
),
(
"center limited string",
Token::text("Ahhh this won't go far")
.limited(10)
.centered(),
vec![
Component::X((WIDTH - 10) / 2),
Component::String("Ahhh this ".into()),
],
),
(
"padded string with underscores is centered",
Token::text("It was...").pad_char('_', 15).centered(),
vec![
Component::X((WIDTH - 15) / 2),
Component::String("It was...______".into()),
],
),
(
"prefixes color",
Token::text("this is red").fg(Color::RED),
vec![
Component::Fg(Color::RED),
Component::String("this is red".into()),
],
),
(
"color at beginning of line",
Token::text("colored").centered().bg(Color::RED),
vec![
Component::Bg(Color::RED),
Component::X((WIDTH - 7) / 2),
Component::String("colored".into()),
],
),
(
"color isnt part of limit",
Token::text("ten chars!")
.bg(Color::RED)
.fg(Color::GREEN)
.bg(Color::BLUE)
.fg(Color::BLACK)
.limited(10),
vec![
Component::Fg(Color::BLACK),
Component::Bg(Color::BLUE),
Component::Fg(Color::GREEN),
Component::Bg(Color::RED),
Component::String("ten chars!".into()),
],
),
(
"multicolor string centered",
Token::text("end")
.fg(Color::RED)
.string("mid")
.fg(Color::BLUE)
.string("start")
.fg(Color::GREEN)
.centered(),
vec![
Component::X((WIDTH - 11) / 2),
Component::Fg(Color::GREEN),
Component::String("start".into()),
Component::Fg(Color::BLUE),
Component::String("mid".into()),
Component::Fg(Color::RED),
Component::String("end".into()),
],
),
]
.into_iter()
.for_each(|(name, token, expected)| {
eprintln!("running test <{}>", &name);
let actual = token.with_width(WIDTH);
assert!(
expected.len() == actual.len(),
"<{}>: length mismatch. expected {} actual {}",
&name,
expected.len(),
actual.len(),
);
for (i, exp) in expected.into_iter().enumerate() {
let act = &actual[i];
assert!(
exp == *act,
"<{}>: component at index {} mismatch.
expected:\n{:#?}
actual:\n{:#?}",
&name,
i,
exp,
act,
);
}
})
}
}