feat: Added history tab, and event passing

This commit is contained in:
Nickiel12 2023-06-01 20:36:56 -07:00
parent 6ef492ee10
commit 7d1613b0c8
7 changed files with 327 additions and 138 deletions

View file

@ -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::new_transaction::NewTransactionTabState;
use crate::uis::navigation_frame::NavigationState;
pub type AppResult<T> = std::result::Result<T, Box<dyn std::error::Error>>; pub type AppResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;
pub enum FocusedBlock { pub struct StatefulList<T> {
Navigation, pub state: ListState,
Body, pub items: Vec<T>,
}
impl<T> StatefulList<T> {
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
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 struct App<'a> {
pub running: bool, 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> { impl<'a> App<'a> {
pub fn new() -> App<'a> { pub fn new() -> App<'a> {
App { App {
running: true, running: true,
states: States::new(),
tabs: vec!["History", "Tab2", "Tab3"], input_mode: InputMode::Normal,
tab_index: 0,
focus: FocusedBlock::Navigation,
new_transaction_tab_state: NewTransactionTabState::new(),
} }
} }
pub fn next_tab(&mut self) { pub fn poll_events(&mut self) -> AppResult<()> {
self.tab_index = (self.tab_index + 1) % self.tabs.len(); 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
pub fn prev_tab(&mut self) { if let KeyCode::Char('q') = key.code {
if self.tab_index > 0 { if let Some(_) = self.states.nav_state.message.clone() {
self.tab_index -= 1; self.states.nav_state.message = None;
} else { } else {
self.tab_index = self.tabs.len() - 1; if let ActiveFrame::Navigation = self.states.active_frame {
self.running = false;
} else {
self.states.active_frame = ActiveFrame::Navigation;
} }
} }
pub fn focus_navigation(&mut self) {
self.focus = FocusedBlock::Navigation;
} }
match self.states.active_frame {
pub fn focus_body(&mut self) { ActiveFrame::Navigation => {
self.focus = FocusedBlock::Body; 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,
}
} }
} }

View file

@ -7,9 +7,8 @@ use ratatui::{
Terminal, Terminal,
}; };
use recount::app::{App, AppResult, FocusedBlock}; use recount::app::{App, AppResult};
use recount::tui::Tui; use recount::tui::Tui;
use recount::uis::new_transaction::NewTransactionTabState;
fn main() -> AppResult<()> { fn main() -> AppResult<()> {
// Create an application. // Create an application.
@ -28,34 +27,7 @@ fn main() -> AppResult<()> {
tui.draw(&mut app)?; tui.draw(&mut app)?;
// Handle events. // Handle events.
if let Event::Key(key) = event::read()? { app.poll_events()?;
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(),
_ => {}
}
}
}
}
}
std::thread::sleep(Duration::from_millis(20)); std::thread::sleep(Duration::from_millis(20));
} }

View file

@ -1,79 +1,52 @@
use crate::app::App; use crate::{app::{App, ActiveFrame}, uis::render_history_tab};
use ratatui::{ use ratatui::{
backend::Backend, backend::Backend,
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style}, widgets::{Block, Borders},
text::{Line, Text}, Frame, style::Style,
widgets::{Block, Borders, Tabs, Paragraph},
Frame,
}; };
use crate::uis::{render_new_transaction_tab, render_history_tab}; use crate::uis::{render_navigation_frame, render_new_transaction_tab};
pub fn render<B: Backend> (f: &mut Frame<B>, app: &App) { pub fn render<B: Backend> (f: &mut Frame<B>, app: &mut App) {
let size = f.size(); let size = f.size();
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .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); .split(size);
let bottom_block = Block::default() 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]); 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() let bottom_chunk = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.margin(1) .margin(1)
.constraints([Constraint::Percentage(100)]) .constraints([Constraint::Percentage(100)])
.split(chunks[1]); .split(chunks[1]);
// if app.tabs selected == history,
//render_history_tab(f, bottom_chunk[0], app); 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); render_new_transaction_tab(f, bottom_chunk[0], app);
} }
_ => {
pub fn render_statusbar<B: Backend> (f: &mut Frame<B>, status_rect: Rect, app: &App) { panic!("ui.rs Render encountered a thought impossible state")
}
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]);
} }

View file

@ -1,12 +1,56 @@
use ratatui::{backend::Backend, Frame, layout::Rect, widgets::Paragraph, text::Text, style::{Style, Color}}; use crossterm::event::{KeyEvent, KeyEventKind, KeyCode};
use crate::app::App; 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<B: Backend> (f: &mut Frame<B>, status_rect: Rect, app: &App) { pub struct HistoryState {
let connection_paragraph = Paragraph::new( pub transacts_list: StatefulList<String>
Text::styled("I'm a ghost!", }
Style::default().fg(Color::Green)
) impl HistoryState {
); pub fn new() -> HistoryState {
f.render_widget(connection_paragraph, status_rect); 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<B: Backend> (f: &mut Frame<B>, body_rect: Rect, app: &mut App) {
// Iterate through all elements in the `items` app and append some debug text to it.
let items: Vec<ListItem> = 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)
} }

View file

@ -5,3 +5,6 @@ pub use self::history::render_history_tab;
pub mod new_transaction; pub mod new_transaction;
pub use self::new_transaction::render_new_transaction_tab; pub use self::new_transaction::render_new_transaction_tab;
pub mod navigation_frame;
pub use self::navigation_frame::render_navigation_frame;

120
src/uis/navigation_frame.rs Normal file
View file

@ -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<String>,
}
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<ActiveFrame> {
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<B: Backend> (f: &mut Frame<B>, 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);
}

View file

@ -1,5 +1,5 @@
use crate::app::App; 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}}; use ratatui::{backend::Backend, Frame, layout::{Rect, Layout, Direction, Constraint}, widgets::{Paragraph, Borders, Block}, text::Text, style::{Style, Color}};
pub struct NewTransactionTabState<'a> { pub struct NewTransactionTabState<'a> {
@ -10,7 +10,7 @@ pub struct NewTransactionTabState<'a> {
impl<'a> NewTransactionTabState<'a> { impl<'a> NewTransactionTabState<'a> {
pub fn new() -> NewTransactionTabState<'a> { pub fn new() -> NewTransactionTabState<'a> {
NewTransactionTabState { NewTransactionTabState {
cur_tab_index: 1, cur_tab_index: 0,
tabs: vec!["Quick Entry", "Manual Entry"] tabs: vec!["Quick Entry", "Manual Entry"]
} }
} }
@ -22,14 +22,10 @@ impl<'a> NewTransactionTabState<'a> {
pub fn handle_event(event: KeyEvent, app: &mut App) { pub fn handle_event(event: KeyEvent, app: &mut App) {
if event.kind == KeyEventKind::Press { if event.kind == KeyEventKind::Press {
match event.code { match event.code {
KeyCode::Char('q') => app.focus_navigation(), KeyCode::Tab => app.states.transactions.next_tab(),
KeyCode::Tab => app.new_transaction_tab_state.next_tab(),
// KeyCode::Right => app.next(),
// KeyCode::Left => app.previous(),
_ => {} _ => {}
} }
} }
} }
} }
@ -42,8 +38,8 @@ pub fn render_new_transaction_tab<B: Backend> (f: &mut Frame<B>, body_rect: Rect
// Render the custom tab bar // Render the custom tab bar
let mut constraints: Vec<Constraint> = vec![]; let mut constraints: Vec<Constraint> = vec![];
let tab_percent: u16 = (100 / app.new_transaction_tab_state.tabs.len()) as u16; let tab_percent: u16 = (100 / app.states.transactions.tabs.len()) as u16;
for _ in 0..app.new_transaction_tab_state.tabs.len() { for _ in 0..app.states.transactions.tabs.len() {
constraints.push(Constraint::Percentage(tab_percent)); constraints.push(Constraint::Percentage(tab_percent));
} }
@ -52,10 +48,10 @@ pub fn render_new_transaction_tab<B: Backend> (f: &mut Frame<B>, body_rect: Rect
.constraints(constraints) .constraints(constraints)
.split(chunks[0]); .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( 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) Style::default().fg(Color::White)
) )
) )
@ -64,7 +60,7 @@ pub fn render_new_transaction_tab<B: Backend> (f: &mut Frame<B>, body_rect: Rect
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.style({ .style({
if app.new_transaction_tab_state.cur_tab_index == i { if app.states.transactions.cur_tab_index == i {
Style::default().bg(Color::Blue) Style::default().bg(Color::Blue)
} else { } else {
Style::default() Style::default()