use crate::{ theme::{Color, ColorSet}, token::Token, }; #[derive(Eq, Debug, Clone)] pub enum Component { X(usize), String(String), Fg(Color), Bg(Color), } 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, }, } } } #[derive(Debug, Clone)] pub struct Line { color: ColorSet, components: Vec, } #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum SectionWidth { Full, Third, TwoThirds, } impl SectionWidth { pub fn abs_size(&self, max_width: usize) -> usize { match self { SectionWidth::Full => max_width, SectionWidth::Third => max_width / 3, SectionWidth::TwoThirds => (max_width / 3) * 2, } } } #[derive(Debug, Clone, Eq)] pub struct Widget { want_width: SectionWidth, 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: SectionWidth, tokens_per_line: Vec, ) -> Self { Self { want_width: width, per_line: tokens_per_line, } } fn get_line(&self, line: usize) -> Option<&Token> { self.per_line.get(line - 1) } } #[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 make( self, (term_width, term_height): (u16, u16), ) -> String { todo!() } 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.abs_size(100)) .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 line(self, widgets: Vec) -> Self { self.fixed(1, widgets) } 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) } // fn make_line(&self, term_width: u16, ) // pub fn make( // &self, // (term_width, term_height): (u16, u16), // ) -> String { // } } #[cfg(test)] mod tests { use super::*; #[test] fn test_plan_to_instructions() { const HEIGHT: usize = 40; let (widget_1, widget_2, widget_3) = ( Widget::new( SectionWidth::Third, vec![Token::text("hello")], ), Widget::new( SectionWidth::Third, vec![Token::text("hello") .pad_char('*', 16) .bg(Color::RED)], ), Widget::new( SectionWidth::Third, vec![Token::text("hello").limited(16).padded(20)], ), ); 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, ); }) } }