use termion::raw::RawTerminal; use crate::{ theme::{Color, ColorSet}, token::Token, }; use std::io::{Stdout, Write}; #[derive(Eq, Clone, Debug)] pub enum Component { NextLine, Link(String, usize), X(usize), String(String), Fg(Color), Bg(Color), } impl Component { fn debug(&self) -> String { match self { Component::NextLine => "NextLine".to_string(), Component::X(x) => format!("X({})", x), Component::String(s) => format!("\"{}\"", s), Component::Fg(c) => format!("FG({:#?})", c) .replace('\n', "") .replace(' ', ""), Component::Bg(c) => format!("BG({:#?})", c) .replace('\n', "") .replace(' ', ""), Component::Link(name, ln) => { format!("Clickable[{}:{}]", name, ln) } } } } pub struct LineArea { pos: (u16, u16), length: u16, } impl LineArea { pub fn new(pos: (u16, u16), length: u16) -> Self { Self { pos, length } } pub fn matches(&self, pos: (u16, u16)) -> bool { self.pos.1 == pos.1 && pos.0 >= self.pos.0 && pos.0 <= (self.pos.0 + self.length - 1) } } impl PartialEq for Component { fn eq(&self, other: &Self) -> bool { match self { Self::X(x) => match other { Self::X(other_x) => x == other_x, _ => false, }, Self::String(s) => match other { Self::String(other_s) => s == other_s, _ => false, }, Self::Fg(c) => match other { Self::Fg(other_c) => c == other_c, _ => false, }, Self::Bg(c) => match other { Self::Bg(other_c) => c == other_c, _ => false, }, Self::NextLine => match other { Self::NextLine => true, _ => false, }, Self::Link(name, length) => match other { Self::Link(other_name, other_length) => { name == other_name && length == other_length } _ => false, }, } } } #[derive(Debug, Clone)] pub struct Line { color: ColorSet, components: Vec, } #[derive(Debug, Clone, Eq)] pub struct Widget { want_width: u8, per_line: Vec, } impl PartialEq for Widget { fn eq(&self, other: &Self) -> bool { self.want_width == other.want_width && self.per_line.len() == other.per_line.len() && (&self.per_line) .into_iter() .enumerate() .all(|(i, token)| token.eq(&other.per_line[i])) } } impl Widget { pub fn new(width_pct: u8, tokens_per_line: Vec) -> Self { Self { want_width: width_pct, per_line: tokens_per_line, } } fn get_line(&self, line: usize) -> Option<&Token> { self.per_line.get(line) } } #[derive(Debug, Clone, Eq)] pub enum Instruction { FixedHeight(Box, usize, Vec), End, } impl PartialEq for Instruction { fn eq(&self, other: &Self) -> bool { match self { Instruction::FixedHeight(next, size, widgets) => { match other { Instruction::FixedHeight( other_next, other_size, other_widgets, ) => { if size == other_size && next.eq(other_next) && widgets.len() == other_widgets.len() { widgets.into_iter().enumerate().all( |(i, widget)| { widget.eq(&other_widgets[i]) }, ) } else { false } } _ => false, } } Instruction::End => match other { Instruction::End => true, _ => false, }, } } } impl Instruction { pub fn into_components( self, line_width: usize, height_left: usize, color_reset: ColorSet, ) -> Vec { if height_left == 0 { return vec![]; } match self { Instruction::FixedHeight(next, lines, wdg) => (0..lines) .map(|line| { let mut offset = 0; (&wdg) .into_iter() .map(|w| { let (width, token) = ( (w.want_width as usize * line_width) / 100 + 1, // Feels wrong? w.get_line(line).map(|t| t.clone()), ); let mut result = match token { Some(tok) => tok .with_width(width) .into_iter() .map(|comp| { if let Component::X(x) = comp { Component::X(offset + x) } else { comp } }) .collect(), None => Vec::new(), }; offset += width; if offset < line_width { result.push(Component::X(offset)); } result .push(Component::Fg(color_reset.fg)); result .push(Component::Bg(color_reset.bg)); result }) .flatten() .chain(vec![Component::NextLine]) .collect::>() }) .flatten() .chain(next.into_components( line_width, height_left - lines, color_reset, )) .collect(), Instruction::End => (0..height_left) .map(|_| Component::NextLine) .collect(), } } pub fn start() -> Self { Self::End } pub fn fixed(self, height: usize, widgets: Vec) -> Self { Self::FixedHeight(Box::new(self), height, widgets) } } #[derive(Clone, Debug)] pub enum Plan { FixedHeight(Box, usize, Vec), Fill(Box, Vec), End, } impl Plan { pub fn start() -> Self { Self::End } fn fit_check(widgets: &Vec) { if widgets .into_iter() .map(|wd| wd.want_width as usize) .sum::() > 100 { panic!("widgets do not fit screen") } } fn to_instruction_set(self, max_height: usize) -> Instruction { let fill_height = { let (reserved_lines, fill_count) = self.count(); (max_height - reserved_lines) / 1.max(fill_count) }; self.to_instruction_fixed_fill_height(fill_height) } fn to_instruction_fixed_fill_height( self, fill_height: usize, ) -> Instruction { match self { Plan::FixedHeight(next, height, widgets) => { Instruction::FixedHeight( Box::new(next.to_instruction_fixed_fill_height( fill_height, )), height, widgets, ) } Plan::Fill(next, widgets) => { Instruction::FixedHeight( Box::new(next.to_instruction_fixed_fill_height( fill_height, )), fill_height, widgets, ) } Plan::End => Instruction::End, } } // counts how many lines are fixed height, those lines // are reserved, and the rest may be split between fills. // the return value is (reserved_lines, fill_count) // so, fill_height should be reserved_lines/fill_count fn count(&self) -> (usize, usize) { match self { Plan::FixedHeight(next, h, _) => { let next = next.count(); (next.0 + h, next.1) } Plan::Fill(next, _) => { let next = next.count(); (next.0, next.1 + 1) } Plan::End => (0, 0), } } pub fn fixed(self, height: usize, widgets: Vec) -> Self { Self::fit_check(&widgets); Self::FixedHeight(Box::new(self), height, widgets) } pub fn fill(self, widgets: Vec) -> Self { Self::fit_check(&widgets); Self::Fill(Box::new(self), widgets) } pub fn make( self, base_colors: ColorSet, (term_width, term_height): (usize, usize), ) -> Vec { self.to_instruction_set(term_height - 1).into_components( term_width, term_height - 1, base_colors, ) } } #[cfg(test)] mod tests { use super::*; const THIRD: u8 = 100 / 3; fn test_widgets() -> (Widget, Widget, Widget) { ( Widget::new(THIRD, vec![Token::text("hello").centered()]), Widget::new( THIRD, vec![Token::text("hello") .pad_char('*', 16) .bg(Color::RED)], ), Widget::new( THIRD, vec![Token::text("hello").limited(16).padded(20)], ), ) } #[test] fn test_instructions_to_components() { const WIDTH: usize = 120; const HEIGHT: usize = 40; const BASE_COLOR: ColorSet = ColorSet { fg: Color::WHITE, bg: Color::BLACK, }; let (w1, w2, w3) = test_widgets(); vec![ ( "all the widgets, 30 lines", Instruction::start().fixed( 30, vec![w1.clone(), w2.clone(), w3.clone()], ), w1.clone() .get_line(0) .unwrap() .clone() .with_width(WIDTH / 3) .into_iter() .chain(vec![ Component::X(WIDTH / 3), Component::Fg(BASE_COLOR.fg), Component::Bg(BASE_COLOR.bg), ]) .chain( w2.clone() .get_line(0) .unwrap() .clone() .with_width(WIDTH / 3), ) .chain(vec![ Component::X((WIDTH / 3) * 2), Component::Fg(BASE_COLOR.fg), Component::Bg(BASE_COLOR.bg), ]) .chain( w3.clone() .get_line(0) .unwrap() .clone() .with_width(WIDTH / 3), ) .chain(vec![ Component::Fg(BASE_COLOR.fg), Component::Bg(BASE_COLOR.bg), Component::NextLine, ]) .chain( (1..30) .map(|_| { vec![ Component::X(WIDTH / 3), Component::Fg(BASE_COLOR.fg), Component::Bg(BASE_COLOR.bg), Component::X((WIDTH / 3) * 2), Component::Fg(BASE_COLOR.fg), Component::Bg(BASE_COLOR.bg), Component::Fg(BASE_COLOR.fg), Component::Bg(BASE_COLOR.bg), Component::NextLine, ] }) .flatten(), ) .chain( (0..(HEIGHT - 30)) .map(|_| Component::NextLine), ) .collect::>(), ), ( "Single widget, single 10 lines section", Instruction::start().fixed(10, vec![w1.clone()]), w1.clone() .get_line(0) .unwrap() .clone() .with_width(WIDTH / 3) .into_iter() .chain( (0..10) .map(|_| { vec![ Component::X(WIDTH / 3), Component::Fg(BASE_COLOR.fg), Component::Bg(BASE_COLOR.bg), Component::NextLine, ] }) .flatten(), ) .chain( (0..(HEIGHT - 10)) .map(|_| Component::NextLine), ) .collect(), ), ] .into_iter() .for_each(|(name, instruction, expected)| { let actual = instruction .into_components(WIDTH, HEIGHT, BASE_COLOR); let diff = actual .clone() .into_iter() .zip(expected.clone()) .enumerate() // .filter(|(i, (left, right))| left != right) .map(|(i, (act, exp))| { format!( "{}{}: {} || {}", if act != exp { "## " } else { "" }, i, exp.debug(), act.debug(), ) .replace('\n', " ") }) .collect::>(); assert!( expected == actual, "<{}>: expected({}) actual({}) diff:\n{:#?}", name, expected.len(), actual.len(), diff, // expected, // actual ); }); } #[test] fn test_plan_to_instructions() { const HEIGHT: usize = 40; let (widget_1, widget_2, widget_3) = test_widgets(); vec![ ("end -> end", Plan::start(), Instruction::End), ( "fill entire screen", Plan::start().fill(vec![]), Instruction::start().fixed(HEIGHT, vec![]), ), ( "5 | fill | 5 -> 5 | HEIGHT - 5 - 5 | 5", Plan::start() .fixed(5, vec![widget_1.clone()]) .fill(vec![widget_3.clone()]) .fixed(5, vec![widget_2.clone()]), Instruction::start() .fixed(5, vec![widget_1.clone()]) .fixed(HEIGHT - 5 - 5, vec![widget_3.clone()]) .fixed(5, vec![widget_2.clone()]), ), ( "5 | 5 | fill -> 5 | 5 | HEIGHT - 5 - 5", Plan::start() .fixed(5, vec![widget_3.clone()]) .fixed(5, vec![widget_2.clone()]) .fill(vec![widget_1.clone()]), Instruction::start() .fixed(5, vec![widget_3.clone()]) .fixed(5, vec![widget_2.clone()]) .fixed(HEIGHT - 5 - 5, vec![widget_1.clone()]), ), ( "fill -> HEIGHT", Plan::start().fill(vec![widget_1.clone()]), Instruction::start() .fixed(HEIGHT, vec![widget_1.clone()]), ), ( " 5 | fill | 5 | fill | 5 -> 5 | (HEIGHT - 15) / 2 | 5 | (HEIGHT - 15) / 2 | 5\n", Plan::start() .fixed( 5, vec![widget_1.clone(), widget_2.clone()], ) .fill(vec![widget_1.clone(), widget_3.clone()]) .fixed(5, vec![]) .fill(vec![widget_2.clone()]) .fixed( 5, vec![widget_3.clone(), widget_2.clone()], ), Instruction::start() .fixed( 5, vec![widget_1.clone(), widget_2.clone()], ) .fixed( (HEIGHT - 15) / 2, vec![widget_1.clone(), widget_3.clone()], ) .fixed(5, vec![]) .fixed((HEIGHT - 15) / 2, vec![widget_2.clone()]) .fixed( 5, vec![widget_3.clone(), widget_2.clone()], ), ), ] .into_iter() .for_each(|(name, plan, expected)| { eprintln!("running test <{}>", &name); let actual = plan.to_instruction_set(HEIGHT); assert!( expected == actual, "<{}>: not equal! expected:\n{:#?} actual:\n{:#?}", &name, expected, actual, ); }) } }