416 lines
14 KiB
Rust
416 lines
14 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),
|
|
PadPercent(Box<Token>, u8),
|
|
Fg(Box<Token>, Color),
|
|
Bg(Box<Token>, Color),
|
|
Link(Box<Token>, String),
|
|
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 pad_percent(self, pct: u8) -> Self {
|
|
#[cfg(debug_assertions)]
|
|
if pct > 100 {
|
|
panic!("pad_percent with >100% width: {}", pct);
|
|
}
|
|
Self::PadPercent(Box::new(self), 100.min(pct))
|
|
}
|
|
|
|
// Returns (str_count, str_len_total)
|
|
pub fn str_len(&self, total_width: usize) -> (usize, usize) {
|
|
match self {
|
|
Token::String(t, s) => {
|
|
let (count, len) = t.str_len(total_width);
|
|
(count + 1, len + s.len())
|
|
}
|
|
Token::Fg(t, _) => t.str_len(total_width),
|
|
Token::Bg(t, _) => t.str_len(total_width),
|
|
Token::Link(t, _) => t.str_len(total_width),
|
|
Token::Centered(t) => t.str_len(total_width),
|
|
Token::Limited(t, lim) => {
|
|
let (count, len) = t.str_len(total_width);
|
|
(count, (*lim).min(len))
|
|
}
|
|
Token::CharPad(t, _, pad_to) => {
|
|
let (count, len) = t.str_len(total_width);
|
|
(count, (*pad_to).max(len))
|
|
}
|
|
Token::End => (0, 0),
|
|
Token::PadPercent(t, pct) => {
|
|
let (count, len) = t.str_len(total_width);
|
|
(
|
|
count,
|
|
((*pct as usize * total_width) / 100).max(len),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Checks if any children are Token::Centered
|
|
// Used for links so they do not get set incorrectly
|
|
fn has_centered(&self) -> bool {
|
|
match self {
|
|
Token::Centered(_) => true,
|
|
Token::Limited(next, _) => next.has_centered(),
|
|
Token::CharPad(next, _, _) => next.has_centered(),
|
|
Token::Fg(next, _) => next.has_centered(),
|
|
Token::Bg(next, _) => next.has_centered(),
|
|
Token::Link(next, _) => next.has_centered(),
|
|
Token::String(next, _) => next.has_centered(),
|
|
Token::End => false,
|
|
Token::PadPercent(next, _) => next.has_centered(),
|
|
}
|
|
}
|
|
|
|
pub fn link<S>(self, name: S) -> Self
|
|
where
|
|
S: Into<String>,
|
|
{
|
|
#[cfg(debug_assertions)]
|
|
{
|
|
if self.has_centered() {
|
|
panic!("FIXME: Token::link called after centered");
|
|
}
|
|
}
|
|
Self::Link(Box::new(self), name.into())
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
pub 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::Link(t, name) => {
|
|
vec![Component::Link(name, t.str_len(width).1)]
|
|
.into_iter()
|
|
.chain(t.with_width(width))
|
|
.collect()
|
|
}
|
|
Token::Centered(cnt) => {
|
|
let mut str_len = cnt.str_len(width).1;
|
|
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![],
|
|
Token::PadPercent(next, pct) => {
|
|
// FIXME: I feel like this'll be a problem
|
|
// at some point but I cba to deal with it rn
|
|
let (str_cnt, str_len) = next.str_len(width);
|
|
let actual_pad = (pct as usize * width) / 100;
|
|
let mut str_idx = 0;
|
|
next.with_width(width)
|
|
.into_iter()
|
|
.map(|comp| {
|
|
if let Component::String(s) = comp {
|
|
str_idx += 1;
|
|
if str_idx != str_cnt {
|
|
// Continue as this isn't the end
|
|
Component::String(s)
|
|
} else if str_len >= actual_pad {
|
|
// FIXME: this is probably THE BAD
|
|
Component::String({
|
|
let mut s = s;
|
|
s.truncate(
|
|
actual_pad
|
|
- (str_len - s.len()),
|
|
);
|
|
s
|
|
})
|
|
} else {
|
|
Component::String(format!(
|
|
"{}{}",
|
|
s,
|
|
" ".repeat(actual_pad - str_len)
|
|
))
|
|
}
|
|
} else {
|
|
comp
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_token_gen() {
|
|
const WIDTH: usize = 100;
|
|
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()),
|
|
],
|
|
),
|
|
(
|
|
"contains a link",
|
|
Token::text("learn more")
|
|
.link("string_link")
|
|
.centered(),
|
|
vec![
|
|
Component::X((WIDTH - 10) / 2),
|
|
Component::Link("string_link".into(), 10),
|
|
Component::String("learn more".into()),
|
|
],
|
|
),
|
|
(
|
|
"padding percentage",
|
|
Token::text("this gets longer").pad_percent(100),
|
|
vec![Component::String(
|
|
"this gets longer".to_string()
|
|
+ &" "
|
|
.repeat(WIDTH - "this gets longer".len()),
|
|
)],
|
|
),
|
|
(
|
|
"pad percent with centering",
|
|
Token::text("all to the middle")
|
|
.pad_percent(60)
|
|
.centered(),
|
|
vec![
|
|
Component::X((WIDTH - (WIDTH * 60) / 100) / 2),
|
|
Component::String(
|
|
"all to the middle".to_string()
|
|
+ &" ".repeat(
|
|
((WIDTH * 60) / 100)
|
|
- "all to the middle".len(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
(
|
|
"two strings, padded pct",
|
|
Token::text("two")
|
|
.bg(Color::BLUE)
|
|
.string("one")
|
|
.pad_percent(80)
|
|
.centered(),
|
|
vec![
|
|
Component::X((WIDTH - ((WIDTH * 80) / 100)) / 2),
|
|
Component::String("one".into()),
|
|
Component::Bg(Color::BLUE),
|
|
Component::String(
|
|
"two".to_string()
|
|
+ &" ".repeat(((WIDTH * 80) / 100) - 6),
|
|
),
|
|
],
|
|
),
|
|
]
|
|
.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.debug(),
|
|
act.debug(),
|
|
);
|
|
}
|
|
})
|
|
}
|
|
}
|