mod container;
mod render_trait;
pub mod widget;
use crate::{config::Config, layout::widget::State, todo::ToDo, ui::HandleEvent};
use anyhow::{Context, Result};
use container::Container;
use core::panic;
use crossterm::event::KeyEvent;
use pest::{iterators::Pair, Parser};
use pest_derive::Parser;
use std::str::FromStr;
use tui::{
layout::{Constraint, Direction, Rect},
Frame,
};
use widget::{new_widget, WidgetType};
#[derive(Parser)]
#[grammar = "./layout/grammar.pest"]
struct LayoutParser;
pub use render_trait::Render;
struct Holder {
container: usize, widgets: Vec<usize>, }
impl Holder {
fn new(l: &Layout) -> Holder {
Holder {
container: l.act,
widgets: l.containers.iter().map(|c| c.get_index()).collect(),
}
}
fn unfocus(&self, l: &mut Layout) {
match l.containers[self.container].get_widget_mut(self.widgets[self.container]) {
Some(widget) if widget.get_base().focus => widget.unfocus(),
_ => {}
}
}
fn set_old_back(&self, l: &mut Layout) {
l.act = self.container;
l.containers
.iter_mut()
.zip(self.widgets.iter())
.for_each(|(c, i)| {
c.set_index(*i);
});
}
}
#[derive(Debug)]
pub struct Layout {
containers: Vec<Container>,
act: usize,
}
impl Layout {
pub fn from_str(template: &str, todo: &ToDo, config: &Config) -> Result<Self> {
fn read_block(
conts: &mut Vec<Container>,
parent: Option<usize>,
block: Pair<'_, Rule>,
config: &Config,
) -> Result<usize> {
let index = conts.len();
conts.push(Container::default());
conts[index].parent = parent;
let direction = match conts[index].parent.map(|i| conts[i].get_direction()) {
Some(Direction::Vertical) => Direction::Horizontal,
_ => Direction::Vertical,
};
conts[index].set_direction(direction);
let mut constrains = vec![];
let mut act_constrain = None;
for inner in block.into_inner() {
use Constraint::*;
use Direction::*;
let get_num = |i: &Pair<'_, Rule>| -> u16 { i.as_str().parse().unwrap() };
match inner.as_rule() {
Rule::directory_horizontal => conts[index].set_direction(Horizontal),
Rule::directory_vertical => conts[index].set_direction(Vertical),
Rule::size_raw => act_constrain = Some(Length(get_num(&inner))),
Rule::size_percentage => act_constrain = Some(Percentage(get_num(&inner))),
Rule::widget => {
let widget_blocks = inner.into_inner().collect::<Vec<_>>();
constrains.push(match widget_blocks.get(1) {
Some(size) if size.as_rule() == Rule::size_raw => {
Constraint::Length(get_num(size))
}
Some(size) if size.as_rule() == Rule::size_percentage => {
Constraint::Percentage(get_num(size))
}
_ => Constraint::Percentage(50),
});
conts[index].add_widget(new_widget(
WidgetType::from_str(widget_blocks[0].as_str())?,
config,
)?);
}
Rule::block => {
constrains.push(act_constrain.take().unwrap_or(Constraint::Percentage(50)));
let next_item = read_block(conts, Some(index), inner, config)?;
conts[index].add_cont(next_item);
}
_ => unreachable!(),
}
}
conts[index].set_constraints(constrains);
Ok(index)
}
let parsed = LayoutParser::parse(Rule::layout, template)
.context("Failed to parse layout")?
.next()
.unwrap();
let outer_block = parsed.into_inner().collect::<Vec<_>>().remove(0);
let mut containers: Vec<Container> = vec![];
read_block(&mut containers, None, outer_block, config)?;
let mut layout = Self { containers, act: 0 };
Container::actualize_layout(&mut layout);
layout.act_mut().actual_mut().unwrap().focus(todo);
Ok(layout)
}
fn act(&self) -> &Container {
&self.containers[self.act]
}
fn act_mut(&mut self) -> &mut Container {
&mut self.containers[self.act]
}
fn walk_in_container(&mut self, f: &impl Fn(&mut Container) -> bool, todo: &ToDo) -> bool {
if f(self.act_mut()) {
Container::actualize_layout(self);
match self.act_mut().actual_mut() {
Some(widget) => widget.focus(todo) || self.walk_in_container(f, todo),
None => true,
}
} else {
false
}
}
fn change_focus(
&mut self,
direction: &Direction,
f: &impl Fn(&mut Container) -> bool,
todo: &ToDo,
) -> bool {
log::trace!(
"Layout::change_focus: direction {:?}, act {}",
&direction,
self.act
);
let old = Holder::new(self);
while *self.act().get_direction() != *direction {
match self.act().parent {
Some(index) => self.act = index,
None => return false,
}
}
if f(self.act_mut()) {
Container::actualize_layout(self);
if match self.act_mut().actual_mut() {
Some(widget) => widget.focus(todo) || self.walk_in_container(f, todo),
None => true,
} {
old.unfocus(self);
true
} else {
log::trace!(
"Revert to cont: {}, widget: {}",
old.container,
old.widgets[old.container]
);
old.set_old_back(self);
false
}
} else {
match self.act().parent {
Some(index) => {
self.act = index;
if self.change_focus(direction, f, todo) {
old.unfocus(self);
true
} else {
old.set_old_back(self);
false
}
}
None => {
old.set_old_back(self);
false
}
}
}
}
fn move_focus(
&mut self,
direction: Direction,
function: fn(&mut Container) -> bool,
todo: &ToDo,
) -> bool {
let ret = self.change_focus(&direction, &function, todo);
Container::actualize_layout(self);
log::debug!(
"Moved: {ret}, act widget: {}, container: {}, position: {}",
self.get_active_widget(),
self.act,
self.act().get_index(),
);
ret
}
pub fn left(&mut self, todo: &ToDo) -> bool {
self.move_focus(Direction::Horizontal, Container::previous_item, todo)
}
pub fn right(&mut self, todo: &ToDo) -> bool {
self.move_focus(Direction::Horizontal, Container::next_item, todo)
}
pub fn up(&mut self, todo: &ToDo) -> bool {
self.move_focus(Direction::Vertical, Container::previous_item, todo)
}
pub fn down(&mut self, todo: &ToDo) -> bool {
self.move_focus(Direction::Vertical, Container::next_item, todo)
}
pub fn handle_key(&mut self, event: &KeyEvent, todo: &mut ToDo) -> bool {
self.act_mut()
.actual_mut()
.expect("Actual is not widget")
.handle_key(event, todo)
}
pub fn get_active_widget(&self) -> WidgetType {
self.act().get_active_type().expect("Actual is not widget")
}
fn find_widget(
&mut self,
find_functor: impl Fn(&&mut Box<dyn State>) -> bool,
process_widget: impl Fn(&mut dyn State),
todo: &ToDo,
) {
let cont_act_index = self.act().get_index();
let indexes = match self
.containers
.iter_mut()
.enumerate()
.flat_map(|(layout_index, container)| {
container
.get_widgets_mut()
.into_iter()
.enumerate()
.map(move |(widget_index, widget)| (layout_index, widget_index, widget))
})
.find(|(_, _, w)| find_functor(w))
{
Some((layout_index, cont_index, widget)) => {
process_widget(widget.as_mut());
if self.act == layout_index && cont_act_index == cont_index {
None
} else if widget.focus(todo) {
Some((layout_index, cont_index))
} else {
None
}
}
None => None,
};
if let Some((layout_index, cont_index)) = indexes {
if let Some(w) = self.act_mut().actual_mut() {
w.unfocus()
}
self.act = layout_index;
self.act_mut().set_index(cont_index);
Container::actualize_parents(self);
}
}
pub fn click(&mut self, column: u16, row: u16, todo: &ToDo) {
log::debug!("Click on column {column}, row {row}");
self.find_widget(
|w| {
let chunk = &w.get_base().chunk;
let x = chunk.x < column && column < chunk.x + chunk.width;
let y = chunk.y < row && row < chunk.y + chunk.height;
x && y
},
|w| w.click(column.into(), row.into(), todo),
todo,
);
}
pub fn select_widget(&mut self, widget_type: WidgetType, todo: &ToDo) {
log::debug!("Select widget {widget_type}");
self.find_widget(|w| w.widget_type() == widget_type, |_| {}, todo);
}
pub fn search(&mut self, to_search: String) {
log::trace!("search to_search={to_search}");
match self.act_mut().actual_mut() {
Some(w) => w.search_event(to_search),
None => panic!("Actual to search is not a widget"),
}
}
pub fn clean_search(&mut self) {
log::trace!("clean_search");
match self.act_mut().actual_mut() {
Some(w) => w.clear_search(),
None => panic!("Actual to search is not a widget"),
}
}
}
impl Render for Layout {
fn render(&self, f: &mut Frame, todo: &ToDo) {
self.containers[0].render(f, &self.containers, todo);
}
fn unfocus(&mut self) {
match self.act_mut().actual_mut() {
Some(w) => w.unfocus(),
None => panic!("Actual to unfocus is not a widget"),
}
}
fn focus(&mut self, todo: &ToDo) -> bool {
match self.act_mut().actual_mut() {
Some(w) => w.focus(todo),
None => panic!("Actual to focus is not a widget"),
}
}
fn update_chunk(&mut self, chunk: Rect) {
Container::update_chunk(chunk, &mut self.containers, 0);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn mock_layout() -> Layout {
let mock_layout = r#"
[
Direction: Horizontal,
Size: 50%,
[
List: 50%,
Preview,
],
[ Direction: Vertical,
Done,
[
Contexts,
Projects,
],
],
]
"#;
Layout::from_str(mock_layout, &ToDo::default(), &Config::default()).unwrap()
}
#[test]
fn test_basic_movement() -> Result<()> {
let mut l = mock_layout();
assert_eq!(l.get_active_widget(), WidgetType::List);
assert!(l.right(&ToDo::default()));
assert_eq!(l.get_active_widget(), WidgetType::Done);
assert!(l.left(&ToDo::default()));
assert_eq!(l.get_active_widget(), WidgetType::List);
assert!(l.right(&ToDo::default()));
assert_eq!(l.get_active_widget(), WidgetType::Done);
assert!(!l.right(&ToDo::default()));
assert_eq!(l.get_active_widget(), WidgetType::Done);
assert!(l.down(&ToDo::default()));
assert_eq!(l.get_active_widget(), WidgetType::Context);
assert!(l.right(&ToDo::default()));
assert_eq!(l.get_active_widget(), WidgetType::Project);
assert!(!l.down(&ToDo::default()));
assert_eq!(l.get_active_widget(), WidgetType::Project);
assert!(l.left(&ToDo::default()));
assert_eq!(l.get_active_widget(), WidgetType::Context);
assert!(l.left(&ToDo::default()));
assert_eq!(l.get_active_widget(), WidgetType::List);
assert!(l.right(&ToDo::default()));
assert_eq!(l.get_active_widget(), WidgetType::Context);
assert!(l.left(&ToDo::default()));
assert_eq!(l.get_active_widget(), WidgetType::List);
assert!(!l.up(&ToDo::default()));
assert_eq!(l.get_active_widget(), WidgetType::List);
Ok(())
}
#[test]
fn test_from_string() -> Result<()> {
let str_layout = r#"
[
dIrEcTiOn:HoRiZoNtAl,
Size: 50%,
List: 50%,
[
Done,
Hashtags: 50%,
],
Projects: 50%,
]
"#;
let mut layout = Layout::from_str(str_layout, &ToDo::default(), &Config::default())?;
assert_eq!(layout.containers.len(), 2);
assert_eq!(*layout.containers[0].get_direction(), Direction::Horizontal);
assert_eq!(layout.containers[0].parent, None);
while layout.containers[0].previous_item() {}
assert_eq!(
layout.containers[0].get_active_type(),
Some(WidgetType::List)
);
assert!(layout.containers[0].next_item());
assert_eq!(layout.containers[0].get_active_type(), None);
assert!(layout.containers[0].next_item());
assert_eq!(
layout.containers[0].get_active_type(),
Some(WidgetType::Project)
);
assert!(!layout.containers[0].next_item());
assert_eq!(*layout.containers[1].get_direction(), Direction::Vertical);
assert_eq!(layout.containers[1].parent, Some(0));
while layout.containers[1].previous_item() {}
assert_eq!(
layout.containers[1].get_active_type(),
Some(WidgetType::Done)
);
assert!(layout.containers[1].next_item());
assert_eq!(
layout.containers[1].get_active_type(),
Some(WidgetType::Hashtag)
);
assert!(!layout.containers[1].next_item());
Ok(())
}
fn assert_rendered(template: &str, width: u16, height: u16, tasks: &[&str], expected: &[&str]) {
let mut todo = ToDo::default();
for task in tasks {
todo.new_task(task).unwrap();
}
let config = Config::default();
let mut layout = Layout::from_str(template, &todo, &config).unwrap();
layout.update_chunk(Rect::new(0, 0, width, height));
let backend = tui::backend::TestBackend::new(width, height);
let mut terminal = tui::Terminal::new(backend).unwrap();
terminal.draw(|f| layout.render(f, &todo)).unwrap();
let buf = terminal.backend().buffer();
let actual: Vec<String> = (0..buf.area.height)
.map(|y| {
(0..buf.area.width)
.map(|x| buf.cell((x, y)).unwrap().symbol().to_string())
.collect()
})
.collect();
let expected: Vec<String> = expected.iter().map(|s| s.to_string()).collect();
assert_eq!(actual, expected, "\nactual:\n{}", actual.join("\n"));
}
#[test]
fn render_single_list() {
assert_rendered(
"[List]",
20,
5,
&[],
&[
"╭list──────────────╮",
"│ │",
"╰──────────────────╯",
" ",
" ",
],
);
}
#[test]
fn render_single_done() {
assert_rendered(
"[Done]",
20,
5,
&[],
&[
"╭done──────────────╮",
"│ │",
"╰──────────────────╯",
" ",
" ",
],
);
}
#[test]
fn render_single_preview() {
assert_rendered(
"[Preview]",
20,
5,
&[],
&[
"╭preview───────────╮",
"│ │",
"╰──────────────────╯",
" ",
" ",
],
);
}
#[test]
fn render_two_widgets_vertical() {
assert_rendered(
"[List, Done]",
20,
8,
&[],
&[
"╭list──────────────╮",
"│ │",
"│ │",
"╰──────────────────╯",
"╭done──────────────╮",
"│ │",
"│ │",
"╰──────────────────╯",
],
);
}
#[test]
fn render_two_widgets_horizontal() {
assert_rendered(
"[Direction: Horizontal, List, Done]",
20,
5,
&[],
&[
"╭list────╮╭done────╮",
"│ ││ │",
"│ ││ │",
"│ ││ │",
"╰────────╯╰────────╯",
],
);
}
#[test]
fn render_three_widgets_vertical() {
assert_rendered(
"[List, Done, Preview]",
20,
9,
&[],
&[
"╭list──────────────╮",
"│ │",
"╰──────────────────╯",
"╭done──────────────╮",
"│ │",
"╰──────────────────╯",
"╭preview───────────╮",
"│ │",
"╰──────────────────╯",
],
);
}
#[test]
fn render_three_widgets_horizontal() {
assert_rendered(
"[Direction: Horizontal, List, Done, Projects]",
30,
5,
&[],
&[
"╭list────╮╭done────╮╭project─╮",
"│ ││ ││ │",
"│ ││ ││ │",
"│ ││ ││ │",
"╰────────╯╰────────╯╰────────╯",
],
);
}
#[test]
fn render_nested_horizontal_in_vertical() {
let template = "[List, [Direction: Horizontal, Done, Projects]]";
assert_rendered(
template,
20,
8,
&[],
&[
"╭list──────────────╮",
"│ │",
"│ │",
"╰──────────────────╯",
"╭done────╮╭project─╮",
"│ ││ │",
"│ ││ │",
"╰────────╯╰────────╯",
],
);
}
#[test]
fn render_nested_vertical_in_horizontal() {
let template = "[Direction: Horizontal, List, [Direction: Vertical, Done, Preview]]";
assert_rendered(
template,
20,
8,
&[],
&[
"╭list────╮╭done────╮",
"│ ││ │",
"│ ││ │",
"│ │╰────────╯",
"│ │╭preview─╮",
"│ ││ │",
"│ ││ │",
"╰────────╯╰────────╯",
],
);
}
#[test]
fn render_with_percentage_sizes() {
let template = "[Direction: Horizontal, List: 30%, Done: 70%]";
assert_rendered(
template,
20,
5,
&[],
&[
"╭list╮╭done────────╮",
"│ ││ │",
"│ ││ │",
"│ ││ │",
"╰────╯╰────────────╯",
],
);
}
#[test]
fn render_with_raw_sizes() {
let template = "[Direction: Horizontal, List: 8, Done: 12]";
assert_rendered(
template,
20,
5,
&[],
&[
"╭list──╮╭done──────╮",
"│ ││ │",
"│ ││ │",
"│ ││ │",
"╰──────╯╰──────────╯",
],
);
}
#[test]
fn render_all_category_widgets() {
let template = "[Direction: Horizontal, Contexts, Projects, Hashtags]";
assert_rendered(
template,
30,
5,
&[],
&[
"╭context─╮╭project─╮╭hashtag─╮",
"│ ││ ││ │",
"│ ││ ││ │",
"│ ││ ││ │",
"╰────────╯╰────────╯╰────────╯",
],
);
}
#[test]
fn render_list_with_tasks() {
assert_rendered(
"[List]",
20,
5,
&["Alpha", "Beta"],
&[
"╭list──────────────╮",
"│Alpha │",
"╰──────────────────╯",
" ",
" ",
],
);
}
#[test]
fn render_horizontal_list_and_done_with_tasks() {
assert_rendered(
"[Direction: Horizontal, List, Done]",
30,
5,
&["Pending task", "x Done task"],
&[
"╭list─────────╮╭done─────────╮",
"│Pending task ││Done task │",
"│ ││ │",
"│ ││ │",
"╰─────────────╯╰─────────────╯",
],
);
}
#[test]
fn render_categories_with_tasks() {
assert_rendered(
"[Direction: Horizontal, Projects, Contexts]",
24,
5,
&["Buy milk +shopping @home", "Code +dev @work"],
&[
"╭project───╮╭context───╮",
"│dev ││home │",
"│shopping ││work │",
"│ ││ │",
"╰──────────╯╰──────────╯",
],
);
}
#[test]
fn render_deeply_nested() {
assert_rendered(
"[Direction: Horizontal, List, [Done, [Direction: Horizontal, Contexts, Projects]]]",
30,
8,
&[],
&[
"╭list─────────╮╭done─────────╮",
"│ ││ │",
"│ ││ │",
"│ │╰─────────────╯",
"│ │╭contex╮╭proje╮",
"│ ││ ││ │",
"│ ││ ││ │",
"╰─────────────╯╰──────╯╰─────╯",
],
);
}
#[test]
fn render_complex_layout() {
assert_rendered(
"[Direction: Horizontal, Size: 50%, [List, Preview], [Direction: Vertical, Done, [Contexts, Projects]]]",
40,
10,
&[],
&[
"╭list──────────────╮╭done──────────────╮",
"│ ││ │",
"│ ││ │",
"│ ││ │",
"╰──────────────────╯╰──────────────────╯",
"╭preview───────────╮╭context─╮╭project─╮",
"│ ││ ││ │",
"│ ││ ││ │",
"│ ││ ││ │",
"╰──────────────────╯╰────────╯╰────────╯",
],
);
}
}