diff --git a/.gitignore b/.gitignore index 2f7896d..7fbb548 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ target/ +# openbsd core dump +*.core diff --git a/kkdisp/src/component.rs b/kkdisp/src/component.rs index 4c0cc55..3728ec6 100644 --- a/kkdisp/src/component.rs +++ b/kkdisp/src/component.rs @@ -1,10 +1,12 @@ -use crate::theme::{Color, ColorSet}; +use crate::{ + theme::{Color, ColorSet}, + token::Token, +}; #[derive(Eq, Debug, Clone)] pub enum Component { X(usize), String(String), - Clear(Clear), Fg(Color), Bg(Color), } @@ -20,10 +22,6 @@ impl PartialEq for Component { Self::String(other_s) => s == other_s, _ => false, }, - Self::Clear(clr) => match other { - Self::Clear(other_clr) => clr == other_clr, - _ => false, - }, Self::Fg(c) => match other { Self::Fg(other_c) => c == other_c, _ => false, @@ -36,8 +34,326 @@ impl PartialEq for Component { } } -#[derive(PartialEq, Debug, Clone, Copy, Eq)] -pub enum Clear { - Line, - Screen, +#[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, + ); + }) + } } diff --git a/kkdisp/src/token.rs b/kkdisp/src/token.rs index 6412dbb..c155093 100644 --- a/kkdisp/src/token.rs +++ b/kkdisp/src/token.rs @@ -1,6 +1,6 @@ use crate::{component::Component, theme::Color}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum Token { Centered(Box), Limited(Box, usize), @@ -236,9 +236,8 @@ mod tests { ), ] .into_iter() - .for_each(|test| { - let (name, token, expected) = test; - eprintln!("token: {:#?}", &token); + .for_each(|(name, token, expected)| { + eprintln!("running test <{}>", &name); let actual = token.with_width(WIDTH); assert!( expected.len() == actual.len(),