use crate::{component::Component, theme::Color}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum Token { Centered(Box), Limited(Box, usize), CharPad(Box, char, usize), Fg(Box, Color), Bg(Box, Color), String(Box, String), End, } impl From 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) -> Self where T: Into, { Self::End.string(t) } pub fn string(self, s: S) -> Self where S: Into, { 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 { 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, ); } }) } }