diff --git a/src/app.rs b/src/app.rs index 134762d..b135dfd 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,53 +1,134 @@ +use crossterm::event::{Event, self, KeyCode}; +use ratatui::widgets::ListState; + +use crate::uis::history::HistoryState; use crate::uis::new_transaction::NewTransactionTabState; +use crate::uis::navigation_frame::NavigationState; pub type AppResult = std::result::Result>; -pub enum FocusedBlock { - Navigation, - Body, +pub struct StatefulList { + pub state: ListState, + pub items: Vec, +} + +impl StatefulList { + pub fn with_items(items: Vec) -> StatefulList { + StatefulList { + state: ListState::default(), + items, + } + } + + pub fn next(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i >= self.items.len() - 1 { + 0 + } else { + i + 1 + } + } + None => 0, + }; + self.state.select(Some(i)); + } + + pub fn previous(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i == 0 { + self.items.len() - 1 + } else { + i - 1 + } + } + None => 0, + }; + self.state.select(Some(i)); + } + + pub fn unselect(&mut self) { + self.state.select(None); + } +} + +pub enum InputMode { + Insert, + Normal, } pub struct App<'a> { pub running: bool, - pub tabs: Vec<&'a str>, - pub tab_index: usize, - pub focus: FocusedBlock, - pub new_transaction_tab_state: NewTransactionTabState<'a>, + pub states: States<'a>, + pub input_mode: InputMode, } impl<'a> App<'a> { pub fn new() -> App<'a> { App { running: true, - - tabs: vec!["History", "Tab2", "Tab3"], - tab_index: 0, - focus: FocusedBlock::Navigation, - - new_transaction_tab_state: NewTransactionTabState::new(), + states: States::new(), + input_mode: InputMode::Normal, } } - pub fn next_tab(&mut self) { - self.tab_index = (self.tab_index + 1) % self.tabs.len(); - } - - pub fn prev_tab(&mut self) { - if self.tab_index > 0 { - self.tab_index -= 1; - } else { - self.tab_index = self.tabs.len() - 1; + pub fn poll_events(&mut self) -> AppResult<()> { + if let Event::Key(key) = event::read()? { + // 'q' needs to be handled at the top level so it can't be + // accidentally handed to a dead end + if let KeyCode::Char('q') = key.code { + if let Some(_) = self.states.nav_state.message.clone() { + self.states.nav_state.message = None; + } else { + if let ActiveFrame::Navigation = self.states.active_frame { + self.running = false; + } else { + self.states.active_frame = ActiveFrame::Navigation; + } + } + } + match self.states.active_frame { + ActiveFrame::Navigation => { + NavigationState::handle_event(key, self); + } + ActiveFrame::History => { + HistoryState::handle_event(key, self); + } + ActiveFrame::NewTransaction => { + NewTransactionTabState::handle_event(key, self); + } + } + } + return Ok(()) + } +} + +pub enum ActiveFrame { + Navigation, + NewTransaction, + History, +} + +pub struct States<'a> { + pub nav_state: NavigationState<'a>, + pub transactions: NewTransactionTabState<'a>, + pub history: HistoryState, + + pub active_frame: ActiveFrame, +} + +impl<'a> States<'a> { + pub fn new() -> States<'a> { + States { + nav_state: NavigationState::new(), + transactions: NewTransactionTabState::new(), + history: HistoryState::new(), + + active_frame: ActiveFrame::Navigation, } - } - - pub fn focus_navigation(&mut self) { - self.focus = FocusedBlock::Navigation; - } - - pub fn focus_body(&mut self) { - self.focus = FocusedBlock::Body; } } diff --git a/src/main.rs b/src/main.rs index 3fbfcfc..c025f7e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,9 +7,8 @@ use ratatui::{ Terminal, }; -use recount::app::{App, AppResult, FocusedBlock}; +use recount::app::{App, AppResult}; use recount::tui::Tui; -use recount::uis::new_transaction::NewTransactionTabState; fn main() -> AppResult<()> { // Create an application. @@ -28,34 +27,7 @@ fn main() -> AppResult<()> { tui.draw(&mut app)?; // Handle events. - if let Event::Key(key) = event::read()? { - match app.focus { - FocusedBlock::Body => { - match app.tab_index { - 2 => { - NewTransactionTabState::handle_event(key, &mut app); - }, - _ => { - // Work around "You entered an empty body, and now, I am dead" loops - app.focus_navigation(); - } - } - - } - FocusedBlock::Navigation => { - if key.kind == KeyEventKind::Press { - match key.code { - KeyCode::Char('q') => break, - KeyCode::Tab => app.next_tab(), - KeyCode::Enter => app.focus_body(), - // KeyCode::Right => app.next(), - // KeyCode::Left => app.previous(), - _ => {} - } - } - } - } - } + app.poll_events()?; std::thread::sleep(Duration::from_millis(20)); } diff --git a/src/ui.rs b/src/ui.rs index fcae848..cec69af 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,79 +1,52 @@ -use crate::app::App; +use crate::{app::{App, ActiveFrame}, uis::render_history_tab}; use ratatui::{ backend::Backend, - layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, - text::{Line, Text}, - widgets::{Block, Borders, Tabs, Paragraph}, - Frame, + layout::{Constraint, Direction, Layout}, + widgets::{Block, Borders}, + Frame, style::Style, }; -use crate::uis::{render_new_transaction_tab, render_history_tab}; +use crate::uis::{render_navigation_frame, render_new_transaction_tab}; -pub fn render (f: &mut Frame, app: &App) { +pub fn render (f: &mut Frame, app: &mut App) { let size = f.size(); let chunks = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) + .constraints([Constraint::Length(3), Constraint::Min(0), Constraint::Length(3)].as_ref()) .split(size); let bottom_block = Block::default() - .borders(Borders::ALL); + .borders(Borders::ALL) + .border_style({ + if let ActiveFrame::Navigation = app.states.active_frame { + Style::default() + } else { + Style::default().fg(ratatui::style::Color::Green) + } + }); f.render_widget(bottom_block, chunks[1]); - render_statusbar(f, chunks[0], app); + render_navigation_frame(f, chunks[0], chunks[2], app); let bottom_chunk = Layout::default() .direction(Direction::Vertical) .margin(1) .constraints([Constraint::Percentage(100)]) .split(chunks[1]); - // if app.tabs selected == history, - //render_history_tab(f, bottom_chunk[0], app); - render_new_transaction_tab(f, bottom_chunk[0], app); -} - -pub fn render_statusbar (f: &mut Frame, status_rect: Rect, app: &App) { - - let status_bar_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(75), Constraint::Min(10)]) - .split(status_rect); - - let left_block = Block::default() - .borders(Borders::ALL); - let right_block = Block::default() - .borders(Borders::ALL); - - - let titles = app - .tabs - .iter() - .cloned() - .map(Line::from) - .collect(); - - let tabs = Tabs::new(titles) - //.block(Block::default().borders(Borders::NONE)) - .block(left_block) - .select(app.tab_index) - //.style(Style::default().fg(Color::Cyan)) - .highlight_style( - Style::default() - .fg(Color::White) - .add_modifier(Modifier::BOLD | Modifier::SLOW_BLINK) - .bg(Color::Blue), - ); - - f.render_widget(tabs, status_bar_chunks[0]); - - let connection_paragraph = Paragraph::new( - Text::styled("Aurora", - Style::default().fg(Color::Green) - ) - ).block(right_block); - - f.render_widget(connection_paragraph, status_bar_chunks[1]); + + if let Some(active_frame) = app.states.nav_state.get_active_tab_frametype() { + match active_frame { + ActiveFrame::History => { + render_history_tab(f, bottom_chunk[0], app); + } + ActiveFrame::NewTransaction => { + render_new_transaction_tab(f, bottom_chunk[0], app); + } + _ => { + panic!("ui.rs Render encountered a thought impossible state") + } + } + } } diff --git a/src/uis/history.rs b/src/uis/history.rs index b6e58e7..e79f71f 100644 --- a/src/uis/history.rs +++ b/src/uis/history.rs @@ -1,12 +1,56 @@ -use ratatui::{backend::Backend, Frame, layout::Rect, widgets::Paragraph, text::Text, style::{Style, Color}}; -use crate::app::App; +use crossterm::event::{KeyEvent, KeyEventKind, KeyCode}; +use ratatui::{backend::Backend, Frame, layout::Rect, widgets::{Block, Borders, List, ListItem}, text::Text, style::{Style, Color, Modifier}}; +use crate::app::{App, StatefulList}; -pub fn render_history_tab (f: &mut Frame, status_rect: Rect, app: &App) { - let connection_paragraph = Paragraph::new( - Text::styled("I'm a ghost!", - Style::default().fg(Color::Green) - ) - ); - f.render_widget(connection_paragraph, status_rect); +pub struct HistoryState { + pub transacts_list: StatefulList +} + +impl HistoryState { + pub fn new() -> HistoryState { + HistoryState { + transacts_list: StatefulList::with_items(vec![ + "Item0".to_string(), + "Item1".to_string(), + "Item2".to_string(), + "Item3".to_string(), + "Item4".to_string(), + ]) + } + } + + pub fn handle_event(event: KeyEvent, app: &mut App) { + + if event.kind == KeyEventKind::Press { + match event.code { + KeyCode::Tab => app.states.transactions.next_tab(), + KeyCode::Up => app.states.history.transacts_list.previous(), + KeyCode::Down => app.states.history.transacts_list.next(), + _ => {} + } + } + } +} + +pub fn render_history_tab (f: &mut Frame, body_rect: Rect, app: &mut App) { + // Iterate through all elements in the `items` app and append some debug text to it. + let items: Vec = app + .states.history.transacts_list + .items + .iter() + .map(|i| { + ListItem::new(Text::from(i.as_str())).style(Style::default().fg(Color::Black).bg(Color::White)) + }) + .collect(); + + let history_items = List::new(items) + .block(Block::default().borders(Borders::NONE)) + .highlight_style( + Style::default() + .bg(Color::Gray) + .add_modifier(Modifier::BOLD) + ); + + f.render_stateful_widget(history_items, body_rect, &mut app.states.history.transacts_list.state) } diff --git a/src/uis/mod.rs b/src/uis/mod.rs index 98d2ced..79c4520 100644 --- a/src/uis/mod.rs +++ b/src/uis/mod.rs @@ -5,3 +5,6 @@ pub use self::history::render_history_tab; pub mod new_transaction; pub use self::new_transaction::render_new_transaction_tab; + +pub mod navigation_frame; +pub use self::navigation_frame::render_navigation_frame; diff --git a/src/uis/navigation_frame.rs b/src/uis/navigation_frame.rs new file mode 100644 index 0000000..d406bb3 --- /dev/null +++ b/src/uis/navigation_frame.rs @@ -0,0 +1,120 @@ +use crossterm::event::{KeyEvent, KeyEventKind, KeyCode}; +use ratatui::{widgets::{Borders, Paragraph, Block, Tabs}, backend::Backend, Frame, style::{Style, Color, Modifier}, text::{Text, Line}, layout::{Rect, Layout, Direction, Constraint}}; + +use crate::app::{App, ActiveFrame}; + +pub struct NavigationState<'a> { + pub tabs: Vec<&'a str>, + pub tab_index: usize, + pub cur_tab_index: usize, + + pub message: Option, +} + +impl<'a> NavigationState<'a> { + pub fn new() -> NavigationState<'a> { + NavigationState { + tabs: vec!["History", "New Transaction"], + tab_index: 0, + cur_tab_index: 0, + message: None, + } + } + + pub fn next_tab(&mut self) { + self.tab_index = (self.tab_index + 1) % self.tabs.len(); + } + + pub fn prev_tab(&mut self) { + if self.tab_index > 0 { + self.tab_index -= 1; + } else { + self.tab_index = self.tabs.len() - 1; + } + } + + pub fn get_active_tab_frametype(&self) -> Option { + match self.tab_index { + 0 => Some(ActiveFrame::History), + 1 => Some(ActiveFrame::NewTransaction), + _ => todo!() + } + } + + pub fn handle_event(event: KeyEvent, app: &mut App) { + if event.kind == KeyEventKind::Press { + match event.code { + KeyCode::Tab => app.states.nav_state.next_tab(), + KeyCode::Enter => app.states.active_frame = app.states.nav_state.get_active_tab_frametype().unwrap(), + _ => {} + } + } + } +} + +pub fn render_navigation_frame (f: &mut Frame, status_rect: Rect, navbar_rect: Rect, app: &App) { + + let status_bar_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(75), Constraint::Min(10)]) + .split(status_rect); + + let left_block = Block::default() + .borders(Borders::ALL) + .border_style({ + if let ActiveFrame::Navigation = app.states.active_frame { + Style::default().fg(ratatui::style::Color::Green) + } else { + Style::default() + } + }); + let right_block = Block::default() + .borders(Borders::ALL); + + + let titles = app.states.nav_state + .tabs + .iter() + .cloned() + .map(Line::from) + .collect(); + + let tabs = Tabs::new(titles) + //.block(Block::default().borders(Borders::NONE)) + .block(left_block) + .select(app.states.nav_state.tab_index) + //.style(Style::default().fg(Color::Cyan)) + .highlight_style( + Style::default() + .fg(Color::White) + .add_modifier(Modifier::BOLD | Modifier::SLOW_BLINK) + .bg(Color::Blue), + ); + + f.render_widget(tabs, status_bar_chunks[0]); + + let connection_paragraph = Paragraph::new( + Text::styled("Aurora", + Style::default().fg(Color::Green) + ) + ).block(right_block); + + f.render_widget(connection_paragraph, status_bar_chunks[1]); + + // Navbar section + let navbar = { + if let None = app.states.nav_state.message { + match app.states.active_frame { + ActiveFrame::Navigation => "Navigating: `q` to exit".to_string(), + _ => "Editing Body: 'q' to go back to navigating".to_string(), + } + } else { + app.states.nav_state.message.clone().unwrap() + } + }; + let bottom_navbar = Paragraph::new( + Text::styled(navbar, Style::default().fg(Color::White)) + ) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(bottom_navbar, navbar_rect); +} diff --git a/src/uis/new_transaction.rs b/src/uis/new_transaction.rs index 421e986..96e5813 100644 --- a/src/uis/new_transaction.rs +++ b/src/uis/new_transaction.rs @@ -1,5 +1,5 @@ use crate::app::App; -use crossterm::event::{Event, KeyEvent, KeyEventKind, KeyCode}; +use crossterm::event::{KeyEvent, KeyEventKind, KeyCode}; use ratatui::{backend::Backend, Frame, layout::{Rect, Layout, Direction, Constraint}, widgets::{Paragraph, Borders, Block}, text::Text, style::{Style, Color}}; pub struct NewTransactionTabState<'a> { @@ -10,7 +10,7 @@ pub struct NewTransactionTabState<'a> { impl<'a> NewTransactionTabState<'a> { pub fn new() -> NewTransactionTabState<'a> { NewTransactionTabState { - cur_tab_index: 1, + cur_tab_index: 0, tabs: vec!["Quick Entry", "Manual Entry"] } } @@ -22,14 +22,10 @@ impl<'a> NewTransactionTabState<'a> { pub fn handle_event(event: KeyEvent, app: &mut App) { if event.kind == KeyEventKind::Press { match event.code { - KeyCode::Char('q') => app.focus_navigation(), - KeyCode::Tab => app.new_transaction_tab_state.next_tab(), - // KeyCode::Right => app.next(), - // KeyCode::Left => app.previous(), + KeyCode::Tab => app.states.transactions.next_tab(), _ => {} } } - } } @@ -42,8 +38,8 @@ pub fn render_new_transaction_tab (f: &mut Frame, body_rect: Rect // Render the custom tab bar let mut constraints: Vec = vec![]; - let tab_percent: u16 = (100 / app.new_transaction_tab_state.tabs.len()) as u16; - for _ in 0..app.new_transaction_tab_state.tabs.len() { + let tab_percent: u16 = (100 / app.states.transactions.tabs.len()) as u16; + for _ in 0..app.states.transactions.tabs.len() { constraints.push(Constraint::Percentage(tab_percent)); } @@ -52,10 +48,10 @@ pub fn render_new_transaction_tab (f: &mut Frame, body_rect: Rect .constraints(constraints) .split(chunks[0]); - for i in 0..app.new_transaction_tab_state.tabs.len() { + for i in 0..app.states.transactions.tabs.len() { let tab = Paragraph::new( - Text::styled(app.new_transaction_tab_state.tabs[i], + Text::styled(app.states.transactions.tabs[i], Style::default().fg(Color::White) ) ) @@ -64,7 +60,7 @@ pub fn render_new_transaction_tab (f: &mut Frame, body_rect: Rect Block::default() .borders(Borders::ALL) .style({ - if app.new_transaction_tab_state.cur_tab_index == i { + if app.states.transactions.cur_tab_index == i { Style::default().bg(Color::Blue) } else { Style::default()