mod container;
mod render_trait;
pub mod widget;
use crate::{
config::Config, layout::widget::State, todo::ToDo, ui::HandleEvent, Result, ToDoError,
};
use container::Container;
use crossterm::event::KeyEvent;
use std::{fmt::Debug, str::FromStr, sync::Arc, sync::Mutex};
use tui::{
layout::{Constraint, Direction, Rect},
Frame,
};
use widget::{widget_type::WidgetType, Widget};
pub use render_trait::Render;
const ITEM_SEPARATOR: char = ',';
const ARG_SEPARATOR: char = ':';
const START_CONTAINER: char = '[';
const END_CONTAINER: char = ']';
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, data: Arc<Mutex<ToDo>>, config: &Config) -> Result<Self> {
fn value_from_string(value: Option<&str>) -> Result<Constraint> {
Ok(match value {
Some(value) => match value.find('%') {
Some(i) if i + 1 < value.len() => {
return Err(ToDoError::ParseUnknownValue(value.to_string()))
}
Some(i) => Constraint::Percentage(value[..i].parse()?),
None => Constraint::Length(value.parse()?),
},
None => Constraint::Percentage(50),
})
}
fn process_item(
item: &str,
container: &mut Container,
data: Arc<Mutex<ToDo>>,
config: &Config,
) -> Result<Option<Constraint>> {
log::trace!("Process item: {item}");
let s = item.to_lowercase();
let x: Vec<&str> = s.splitn(2, ARG_SEPARATOR).map(|s| s.trim()).collect();
let x = (x[0], if x.len() > 1 { Some(x[1]) } else { None });
match x.0 {
"direction" => {
match x.1 {
None | Some("vertical") => container.set_direction(Direction::Vertical),
Some("horizontal") => container.set_direction(Direction::Horizontal),
Some(direction) => {
return Err(ToDoError::ParseInvalidDirection(direction.to_owned()))
}
}
Ok(None)
}
"size" => Ok(Some(value_from_string(x.1)?)),
_ => {
container.add_widget(Widget::new(
WidgetType::from_str(x.0)?,
data.clone(),
config,
)?);
Ok(Some(value_from_string(x.1)?))
}
}
}
let index = match template.find('[') {
Some(i) => i,
None => return Err(ToDoError::ParseNotStart),
};
let template = &template[index + 1..];
log::debug!("Layout from str: {}", template);
let mut string = String::new();
let mut constraints_stack: Vec<Vec<Constraint>> = Vec::new();
constraints_stack.push(Vec::new());
let mut containers: Vec<Container> = Vec::new();
let mut layout = Layout {
act: Container::add_container(&mut containers, Container::default()),
containers,
};
for ch in template.chars() {
match ch {
START_CONTAINER => {
if !string.is_empty() {
return Err(ToDoError::ParseUnknowBeforeContainer(string));
}
if layout.act().item_count() >= constraints_stack.last().unwrap().len() {
constraints_stack
.last_mut()
.unwrap()
.push(Constraint::Percentage(50));
}
let mut cont = Container::default();
cont.parent = Some(layout.act);
cont.set_direction(match layout.act().get_direction() {
Direction::Horizontal => Direction::Vertical,
Direction::Vertical => Direction::Horizontal,
});
layout.act = Container::add_container(&mut layout.containers, cont);
constraints_stack.push(Vec::new());
}
END_CONTAINER => {
log::trace!(
"Act: {}, Constraints: {:?}",
layout.act,
constraints_stack.last()
);
layout
.act_mut()
.set_constraints(constraints_stack.pop().unwrap());
layout.act = match layout.act().parent {
Some(parent) => parent,
None => {
Container::actualize_layout(&mut layout);
layout.act_mut().actual_mut().unwrap().focus();
return Ok(layout);
}
};
string.clear();
}
ITEM_SEPARATOR => {
if !string.is_empty() {
if let Some(constrain) =
process_item(&string, layout.act_mut(), data.clone(), config)?
{
constraints_stack.last_mut().unwrap().push(constrain);
}
string.clear();
}
}
' ' => {}
'\n' => {}
_ => string.push(ch),
};
}
Err(ToDoError::ParseNotEnd)
}
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) -> bool {
if f(self.act_mut()) {
Container::actualize_layout(self);
match self.act_mut().actual_mut() {
Some(widget) => widget.focus() || self.walk_in_container(f),
None => true,
}
} else {
false
}
}
fn change_focus(&mut self, direction: &Direction, f: &impl Fn(&mut Container) -> bool) -> 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() || self.walk_in_container(f),
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) {
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) -> bool {
let ret = self.change_focus(&direction, &function);
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) -> bool {
self.move_focus(Direction::Horizontal, Container::previous_item)
}
pub fn right(&mut self) -> bool {
self.move_focus(Direction::Horizontal, Container::next_item)
}
pub fn up(&mut self) -> bool {
self.move_focus(Direction::Vertical, Container::previous_item)
}
pub fn down(&mut self) -> bool {
self.move_focus(Direction::Vertical, Container::next_item)
}
pub fn handle_key(&mut self, event: &KeyEvent) -> bool {
self.act_mut()
.actual_mut()
.expect("Actual is not widget")
.handle_key(&event.code)
}
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 Widget) -> bool,
process_widget: impl Fn(&mut Widget),
) {
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);
if self.act == layout_index && cont_act_index == cont_index {
None
} else if widget.focus() {
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) {
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()),
);
}
pub fn select_widget(&mut self, widget_type: WidgetType) {
log::debug!("Select widget {widget_type}");
self.find_widget(|w| w.widget_type() == widget_type, |_| {});
}
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) {
self.containers[0].render(f, &self.containers);
}
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) -> bool {
match self.act_mut().actual_mut() {
Some(w) => w.focus(),
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,
Arc::new(Mutex::new(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());
assert_eq!(l.get_active_widget(), WidgetType::Done);
assert!(l.left());
assert_eq!(l.get_active_widget(), WidgetType::List);
assert!(l.right());
assert_eq!(l.get_active_widget(), WidgetType::Done);
assert!(!l.right());
assert_eq!(l.get_active_widget(), WidgetType::Done);
assert!(l.down());
assert_eq!(l.get_active_widget(), WidgetType::Context);
assert!(l.right());
assert_eq!(l.get_active_widget(), WidgetType::Project);
assert!(!l.down());
assert_eq!(l.get_active_widget(), WidgetType::Project);
assert!(l.left());
assert_eq!(l.get_active_widget(), WidgetType::Context);
assert!(l.left());
assert_eq!(l.get_active_widget(), WidgetType::List);
assert!(l.right());
assert_eq!(l.get_active_widget(), WidgetType::Context);
assert!(l.left());
assert_eq!(l.get_active_widget(), WidgetType::List);
assert!(!l.up());
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%,
]
Direction: ERROR,
"#;
let mut layout = Layout::from_str(
str_layout,
Arc::new(Mutex::new(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(())
}
}