From d23f4584443ae68edfe785950b796b6e5d0c90b4 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 16 Jun 2023 15:14:39 +0700 Subject: [PATCH] Redesign UI, project stats panel, show project name, fixes and refactorings --- src/app.rs | 11 +++++- src/input.rs | 16 ++++---- src/ui.rs | 104 +++++++++++++++++++++++++++++++++++---------------- 3 files changed, 90 insertions(+), 41 deletions(-) diff --git a/src/app.rs b/src/app.rs index 26ec9a0..c2fd3d6 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,9 +1,9 @@ use anyhow::Error; +use int_enum::IntEnum; use rusqlite::Connection; use serde::{Deserialize, Serialize}; use std::cmp::min; use tui_textarea::TextArea; -use int_enum::IntEnum; use crate::db; @@ -55,6 +55,7 @@ impl Default for TaskState<'_> { } pub struct State<'a> { + pub project_name: String, pub selected_column_idx: usize, pub columns: Vec, pub db_conn: db::DBConn, @@ -72,7 +73,15 @@ impl<'a> State<'a> { let db_conn = db::DBConn::new(conn); let columns = db_conn.get_all_columns()?; let selected_column = db_conn.get_selected_column()?; + + let project_name = std::env::current_dir()? + .file_name() + .and_then(std::ffi::OsStr::to_str) + .unwrap_or("KANBAN PROJECT") + .to_string(); + Ok(State { + project_name, columns, selected_column_idx: selected_column, quit: false, diff --git a/src/input.rs b/src/input.rs index 2a4721e..30dde92 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,10 +1,10 @@ use crate::app::{State, TaskEditFocus, TaskState, EDIT_WINDOW_FOCUS_STATES}; +use anyhow::Error; use crossterm::event; use crossterm::event::{Event, KeyCode}; use int_enum::IntEnum; -use anyhow::Error; -pub fn cycle_focus(task: &mut TaskState<'_>, forward: bool) -> Result<(), Error>{ +pub fn cycle_focus(task: &mut TaskState<'_>, forward: bool) -> Result<(), Error> { let cycle; if forward { cycle = (task.focus.int_value() + 1) % EDIT_WINDOW_FOCUS_STATES; @@ -21,14 +21,14 @@ pub fn handle_task_edit(state: &mut State<'_>, key: event::KeyEvent) -> Result<( // assign later to task_edit_state let updated_task = if let Some(mut task) = state.task_edit_state.take() { match (key.code, task.focus) { - (KeyCode::Tab, _) => { + (KeyCode::Tab, _) => { cycle_focus(&mut task, true)?; Some(task) - }, + } (KeyCode::BackTab, _) => { cycle_focus(&mut task, false)?; Some(task) - }, + } (KeyCode::Enter, TaskEditFocus::ConfirmBtn) => { // The structure of this function is so we avoid an // unncessary clone() here. We can just transfer @@ -49,12 +49,12 @@ pub fn handle_task_edit(state: &mut State<'_>, key: event::KeyEvent) -> Result<( (_, TaskEditFocus::Title) => { task.title.input(key); Some(task) - }, + } (_, TaskEditFocus::Description) => { task.description.input(key); Some(task) } - _ => Some(task) + _ => Some(task), } } else { None @@ -79,7 +79,7 @@ pub fn handle_main(state: &mut State<'_>, key: event::KeyEvent) -> Result<(), Er KeyCode::Char('n') => Ok(state.task_edit_state = Some(TaskState::default())), KeyCode::Char('e') => Ok(state.task_edit_state = state.get_task_state_from_current()), KeyCode::Char('D') => state.delete_task(), - _ => Ok(()) + _ => Ok(()), } } diff --git a/src/ui.rs b/src/ui.rs index 22d477f..34be8b3 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -35,9 +35,12 @@ fn draw_tasks(f: &mut Frame<'_, B>, area: Rect, state: &State<'_>) { span = Span::raw(&task.title); } span.style = style; + // TODO: This is unoptimized, we can actually construct the list + // inside a single ListItem it seems ListItem::new(vec![Spans::from(span)]) }) .collect(); + let mut style = Style::default(); if i == state.selected_column_idx { style = style.add_modifier(Modifier::REVERSED); @@ -49,8 +52,10 @@ fn draw_tasks(f: &mut Frame<'_, B>, area: Rect, state: &State<'_>) { 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); } @@ -95,7 +100,7 @@ fn centered_rect_for_popup(percent_x: u16, percent_y: u16, r: Rect) -> Rect { .split(popup_layout[1])[1] } -pub fn draw_task_popup(f: &mut Frame<'_, B>, state: &mut State<'_>, popup_title: &str) { +fn draw_task_popup(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) @@ -130,30 +135,22 @@ pub fn draw_task_popup(f: &mut Frame<'_, B>, state: &mut State<'_>, ) .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_style, cancel_style, create_txt, cancel_txt) = match task.focus { + TaskEditFocus::ConfirmBtn => ( + Style::default().add_modifier(Modifier::BOLD), + Style::default(), + "[Confirm]", + " Cancel ", + ), + TaskEditFocus::CancelBtn => ( + Style::default(), + Style::default().add_modifier(Modifier::BOLD), + " Confirm ", + "[Cancel]", + ), + _ => (Style::default(), Style::default(), " Confirm ", " 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]); @@ -194,6 +191,40 @@ pub fn draw_task_popup(f: &mut Frame<'_, B>, state: &mut State<'_>, } } +fn draw_project_stats(f: &mut Frame<'_, B>, area: Rect, state: &mut State<'_>) { + let block = Block::default() + .title("PROJECT STATS") + .borders(Borders::ALL); + + let c1_len = state.columns[0].tasks.len(); + let c2_len = state.columns[1].tasks.len(); + let c3_len = state.columns[2].tasks.len(); + let c4_len = state.columns[3].tasks.len(); + let tocomplete_total = c1_len + c2_len + c3_len; + let percentage = (c3_len as f32 / tocomplete_total as f32 * 100.0) as u8; + let list = List::new( + vec![ + ListItem::new(vec![ + Spans::from("Tasks per Column:"), + Spans::from(format!(" Todo ({})", c1_len)), + Spans::from(format!(" In Progress ({})", c2_len)), + Spans::from(format!(" Done ({})", c3_len)), + Spans::from(format!(" Ideas ({})", c4_len)), + Spans::from( + format!( + "Progress: {} / {} - {}%", + c3_len, + tocomplete_total, + percentage + )) + ] + )] + ) + .block(block); + + f.render_widget(list, area); +} + /// Macro to generate keybindings string at compile time macro_rules! unroll { (($first_a:literal, $first_b:literal), $(($a:literal, $b:literal)),*) => { @@ -206,26 +237,35 @@ pub fn draw(f: &mut Frame<'_, B>, state: &mut State<'_>) { .direction(Direction::Vertical) .constraints( [ - Constraint::Percentage(10), - Constraint::Percentage(65), - Constraint::Percentage(20), - Constraint::Length(3), + Constraint::Length(2), + Constraint::Min(10), + Constraint::Max(10), + Constraint::Length(2), ] .as_ref(), ) .split(f.size()); - let block = Block::default().title("KANBAN BOARD").borders(Borders::ALL); + let block = Block::default() + .title(format!("⎸ {} ⎹", state.project_name)) + .title_alignment(Alignment::Center) + .borders(Borders::TOP); f.render_widget(block, main_layout[0]); draw_tasks(f, main_layout[1], state); - draw_task_info(f, main_layout[2], state); + let info_area = Layout::default() + .direction(Direction::Horizontal) + .constraints(vec![Constraint::Min(60), Constraint::Max(60)].as_ref()) + .split(main_layout[2]); + + draw_task_info(f, info_area[0], state); + draw_project_stats(f, info_area[1], state); let block = Block::default().title("KEYBINDINGS").borders(Borders::TOP); let foot_txt = unroll![ - ("quit", "c"), + ("quit", "q"), ("navigation", "hjkl"), ("move task", "HJKL"), ("new task", "n"),