588 lines
18 KiB
Rust
588 lines
18 KiB
Rust
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<Component>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Eq)]
|
|
pub struct Widget {
|
|
want_width: u8,
|
|
per_line: Vec<Token>,
|
|
}
|
|
|
|
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<Token>) -> 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<Instruction>, usize, Vec<Widget>),
|
|
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<Component> {
|
|
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::<Vec<Component>>()
|
|
})
|
|
.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<Widget>) -> Self {
|
|
Self::FixedHeight(Box::new(self), height, widgets)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum Plan {
|
|
FixedHeight(Box<Plan>, usize, Vec<Widget>),
|
|
Fill(Box<Plan>, Vec<Widget>),
|
|
End,
|
|
}
|
|
|
|
impl Plan {
|
|
pub fn start() -> Self {
|
|
Self::End
|
|
}
|
|
|
|
fn fit_check(widgets: &Vec<Widget>) {
|
|
if widgets
|
|
.into_iter()
|
|
.map(|wd| wd.want_width as usize)
|
|
.sum::<usize>()
|
|
> 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<Widget>) -> Self {
|
|
Self::fit_check(&widgets);
|
|
Self::FixedHeight(Box::new(self), height, widgets)
|
|
}
|
|
|
|
pub fn fill(self, widgets: Vec<Widget>) -> 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<Component> {
|
|
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::<Vec<Component>>(),
|
|
),
|
|
(
|
|
"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::<Vec<String>>();
|
|
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,
|
|
);
|
|
})
|
|
}
|
|
}
|