247 lines
8.8 KiB
Rust
247 lines
8.8 KiB
Rust
use crate::app::{State, TaskEditFocus};
|
|
use tui::backend::Backend;
|
|
use tui::layout::{Alignment, Constraint, Direction, Layout, Rect};
|
|
use tui::style::{Modifier, Style};
|
|
use tui::text::{Span, Spans};
|
|
use tui::widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap};
|
|
use tui::Frame;
|
|
|
|
fn draw_tasks<B: Backend>(f: &mut Frame<'_, B>, area: Rect, state: &State<'_>) {
|
|
let columns = Layout::default()
|
|
.direction(Direction::Horizontal)
|
|
.constraints(
|
|
vec![
|
|
Constraint::Percentage(100 / u16::try_from(state.columns.len()).unwrap());
|
|
state.columns.len()
|
|
]
|
|
.as_ref(),
|
|
)
|
|
.split(area);
|
|
|
|
for (i, column) in state.columns.iter().enumerate() {
|
|
let items: Vec<ListItem<'_>> = column
|
|
.tasks
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(j, task)| {
|
|
let mut style = Style::default();
|
|
let col_idx = state.selected_column_idx;
|
|
let task_idx = state.get_selected_column().selected_task_idx;
|
|
let item_txt;
|
|
if i == col_idx && j == task_idx {
|
|
style = style.add_modifier(Modifier::BOLD | Modifier::UNDERLINED);
|
|
item_txt = format!("{} 👈", task.title);
|
|
} else {
|
|
item_txt = task.title.clone();
|
|
}
|
|
let mut s = Span::raw(item_txt);
|
|
s.style = style;
|
|
ListItem::new(vec![Spans::from(s)])
|
|
})
|
|
.collect();
|
|
let mut style = Style::default();
|
|
if i == state.selected_column_idx {
|
|
style = style.add_modifier(Modifier::REVERSED);
|
|
};
|
|
let mut s = Span::raw(column.name.as_str());
|
|
s.style =
|
|
Style::default().add_modifier(Modifier::BOLD | Modifier::ITALIC | Modifier::UNDERLINED);
|
|
// .fg(Color::White);
|
|
let block = Block::default().title(s).borders(Borders::ALL);
|
|
let inner_area = block.inner(columns[i]);
|
|
let inner_block = Block::default().style(style);
|
|
let list = List::new(items).block(inner_block);
|
|
let mut list_state = ListState::default();
|
|
list_state.select(Some(column.selected_task_idx + 1));
|
|
f.render_widget(block, columns[i]);
|
|
f.render_stateful_widget(list, inner_area, &mut list_state);
|
|
}
|
|
}
|
|
|
|
fn draw_task_info<B: Backend>(f: &mut Frame<'_, B>, area: Rect, state: &State<'_>) {
|
|
let block = Block::default().title("TASK INFO").borders(Borders::ALL);
|
|
if let Some(task) = state.get_selected_column().get_selected_task() {
|
|
let p = Paragraph::new(task.description.as_str())
|
|
.block(block)
|
|
.wrap(Wrap { trim: true });
|
|
f.render_widget(p, area);
|
|
} else {
|
|
let p = Paragraph::new("No tasks for this column").block(block);
|
|
f.render_widget(p, area);
|
|
}
|
|
}
|
|
|
|
fn centered_rect_for_popup(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
|
|
let popup_layout = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints(
|
|
[
|
|
Constraint::Percentage((100 - percent_y) / 2),
|
|
Constraint::Percentage(percent_y),
|
|
Constraint::Percentage((100 - percent_y) / 2),
|
|
]
|
|
.as_ref(),
|
|
)
|
|
.split(r);
|
|
|
|
Layout::default()
|
|
.direction(Direction::Horizontal)
|
|
.constraints(
|
|
[
|
|
Constraint::Percentage((100 - percent_x) / 2),
|
|
Constraint::Percentage(percent_x),
|
|
Constraint::Percentage((100 - percent_x) / 2),
|
|
]
|
|
.as_ref(),
|
|
)
|
|
.split(popup_layout[1])[1]
|
|
}
|
|
|
|
pub fn draw_task_popup<B: Backend>(f: &mut Frame<'_, B>, state: &mut State<'_>, popup_title: &str) {
|
|
let area = centered_rect_for_popup(45, 60, f.size());
|
|
let block = Block::default()
|
|
.title(popup_title)
|
|
.title_alignment(Alignment::Center)
|
|
.borders(Borders::ALL);
|
|
f.render_widget(Clear, area);
|
|
f.render_widget(Paragraph::new("").block(block.clone()), area);
|
|
if let Some(task) = &mut state.task_edit_state {
|
|
let block_inner = block.inner(area);
|
|
let layout = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints(
|
|
[
|
|
Constraint::Length(3),
|
|
Constraint::Max(100),
|
|
Constraint::Length(1),
|
|
Constraint::Length(2),
|
|
]
|
|
.as_ref(),
|
|
)
|
|
.split(block_inner);
|
|
|
|
let buttons = Layout::default()
|
|
.direction(Direction::Horizontal)
|
|
.constraints(
|
|
[
|
|
Constraint::Percentage(80),
|
|
Constraint::Min(10),
|
|
Constraint::Min(10),
|
|
]
|
|
.as_ref(),
|
|
)
|
|
.split(layout[2]);
|
|
|
|
let create_style;
|
|
let create_txt;
|
|
let cancel_style;
|
|
let cancel_txt;
|
|
match task.focus {
|
|
TaskEditFocus::ConfirmBtn => {
|
|
create_style = Style::default().add_modifier(Modifier::BOLD);
|
|
cancel_style = Style::default();
|
|
create_txt = "[Confirm]";
|
|
cancel_txt = " Cancel ";
|
|
}
|
|
TaskEditFocus::CancelBtn => {
|
|
create_style = Style::default();
|
|
cancel_style = Style::default().add_modifier(Modifier::BOLD);
|
|
create_txt = " Confirm ";
|
|
cancel_txt = "[Cancel]";
|
|
}
|
|
_ => {
|
|
create_style = Style::default();
|
|
cancel_style = Style::default();
|
|
create_txt = " Confirm ";
|
|
cancel_txt = " Cancel ";
|
|
}
|
|
}
|
|
let create_btn = Paragraph::new(create_txt).style(create_style);
|
|
let cancel_btn = Paragraph::new(cancel_txt).style(cancel_style);
|
|
f.render_widget(create_btn, buttons[1]);
|
|
f.render_widget(cancel_btn, buttons[2]);
|
|
|
|
let b1 = Block::default().title("Title").borders(Borders::ALL);
|
|
let b2 = Block::default().title("Description").borders(Borders::ALL);
|
|
let b3 = Block::default().title("Keys").borders(Borders::TOP);
|
|
task.title.set_cursor_line_style(Style::default());
|
|
task.description.set_cursor_line_style(Style::default());
|
|
|
|
task.title.set_block(b1);
|
|
if let TaskEditFocus::Title = task.focus {
|
|
task.title
|
|
.set_style(Style::default().add_modifier(Modifier::BOLD));
|
|
task.title
|
|
.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
|
|
} else {
|
|
task.title.set_style(Style::default());
|
|
task.title.set_cursor_style(Style::default());
|
|
}
|
|
f.render_widget(task.title.widget(), layout[0]);
|
|
|
|
task.description.set_block(b2);
|
|
if let TaskEditFocus::Description = task.focus {
|
|
task.description
|
|
.set_style(Style::default().add_modifier(Modifier::BOLD));
|
|
task.description
|
|
.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
|
|
} else {
|
|
task.description.set_style(Style::default());
|
|
task.description.set_cursor_style(Style::default());
|
|
}
|
|
f.render_widget(task.description.widget(), layout[1]);
|
|
|
|
let footer = Paragraph::new("Tab/Backtab : Cycle").block(b3);
|
|
f.render_widget(footer, layout[3]);
|
|
}
|
|
}
|
|
|
|
/// Macro to generate keybindings string at compile time
|
|
macro_rules! unroll {
|
|
(($first_a:literal, $first_b:literal), $(($a:literal, $b:literal)),*) => {
|
|
concat!(concat!($first_a, ": ", $first_b) $(," | ", concat!($a, ": ", $b))*)
|
|
};
|
|
}
|
|
|
|
pub fn draw<B: Backend>(f: &mut Frame<'_, B>, state: &mut State<'_>) {
|
|
let main_layout = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints(
|
|
[
|
|
Constraint::Percentage(10),
|
|
Constraint::Percentage(65),
|
|
Constraint::Percentage(20),
|
|
Constraint::Length(3),
|
|
]
|
|
.as_ref(),
|
|
)
|
|
.split(f.size());
|
|
|
|
let block = Block::default().title("KANBAN BOARD").borders(Borders::ALL);
|
|
f.render_widget(block, main_layout[0]);
|
|
|
|
draw_tasks(f, main_layout[1], state);
|
|
|
|
draw_task_info(f, main_layout[2], state);
|
|
|
|
let block = Block::default().title("KEYBINDINGS").borders(Borders::TOP);
|
|
|
|
let foot_txt = unroll![
|
|
("quit", "c"),
|
|
("navigation", "hjkl"),
|
|
("move task", "HJKL"),
|
|
("new task", "n"),
|
|
("edit task", "e"),
|
|
("cycle edit fields", "Tab"),
|
|
("column top", "g"),
|
|
("column bottom", "G")
|
|
];
|
|
|
|
let footer = Paragraph::new(foot_txt).block(block);
|
|
f.render_widget(footer, main_layout[3]);
|
|
|
|
if state.task_edit_state.is_some() {
|
|
draw_task_popup(f, state, "Create Task");
|
|
}
|
|
}
|