diff --git a/src/app.rs b/src/app.rs index d4f1b32..5d60194 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,30 +1,17 @@ use std::any::Any; use crate::event::{AppEvent, EventHandler}; -use ratatui::widgets::{Block, Borders, Paragraph, StatefulWidget}; use ratatui::{DefaultTerminal, Frame}; use std::time::Duration; use color_eyre::Result; -use crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; -use crossterm::event::KeyCode::Char; +use crossterm::event::{Event, KeyEvent}; use crossterm::event::Event as CrosstermEvent; use diesel::{Connection, SqliteConnection}; -use ratatui::buffer::Buffer; -use ratatui::layout::{Constraint, Direction, Layout, Rect}; -use ratatui::prelude::{Widget}; -use ratatui::style::{Color, Style}; -use ratatui::text::{Line, Span, Text}; use crate::config::types::ApplicationConfig; use crate::constants::{APP_CONFIG_DIR, APP_CONIFG_FILE_PATH, APP_DATA_DIR}; -use crate::widgets::popups::folder::AddFolderPopup; - -enum AppStatus { - Running, - Exiting, - Input -} +use crate::widgets::views::{View}; +use crate::widgets::views::main_view::MainView; pub(crate) struct App { - status: AppStatus, events: EventHandler, db_connection: SqliteConnection, app_config: ApplicationConfig, @@ -32,7 +19,7 @@ pub(crate) struct App { } struct AppState { - popup: Option>, + view: Option>, } impl App { @@ -42,9 +29,8 @@ impl App { else { ApplicationConfig::new() }; Self::initialize_folders(); let db_conn = Self::establish_db_connection(app_conf.clone()); - let state = AppState { popup: None }; + let state = AppState { view: Some(Box::new(MainView::new(&app_conf))) }; Self { - status: AppStatus::Running, events: EventHandler::new(Duration::from_millis(app_conf.basic_config.tick_rate)), db_connection: db_conn, app_config: app_conf, @@ -72,118 +58,48 @@ impl App { terminal.draw(|frame| self.draw(frame))?; let event = self.events.next().await?; self.update(event)?; - if matches!(self.status, AppStatus::Exiting) { + if let Some(view) = self.state.view.as_mut() && + let Some(main_view) = view.downcast_ref::() && + !main_view.is_running() { break Ok(()) } } } fn update(&mut self, event: AppEvent) -> Result<()> { - if matches!(self.status, AppStatus::Input) && let AppEvent::Raw(raw) = &event && - let Some(boxed) = &mut self.state.popup && let Some(popup) = boxed.downcast_mut::() { - popup.textarea.handle_input(raw)?; - } - if let AppEvent::Raw(cross_event) = event && - let CrosstermEvent::Key(key) = cross_event{ - self.handle_key_event(&key)?; + if let AppEvent::Raw(cross_event) = event { + self.handle_event(&cross_event)?; + if let CrosstermEvent::Key(key) = cross_event { + self.handle_key_event(&key)?; + } } + Ok(()) } fn handle_key_event(&mut self, key: &KeyEvent) -> Result<()> { - if matches!(self.status, AppStatus::Input) && matches!(key.code, KeyCode::Esc) { - self.status = AppStatus::Running; + if let Some(any) = self.state.view .as_mut() { + if let Some(main_view) = any.downcast_mut::() { + main_view.handle_key_input(key)?; + } } - if matches!(key.kind, KeyEventKind::Press) && !matches!(self.status, AppStatus::Input) { - match key.code { - Char('q') => self.quit()?, - Char('a') => self.folder_popup(), - _ => {} + Ok(()) + } + + fn handle_event(&mut self, key: &Event) -> Result<()> { + if let Some(any) = self.state.view.as_mut() { + if let Some(main_view) = any.downcast_mut::() { + main_view.handle_input(key)?; } } Ok(()) } fn draw(&mut self, frame: &mut Frame) { - frame.render_widget(self, frame.area()) - } -} - -impl Widget for &mut App { - fn render(self, area: Rect, buf: &mut Buffer) - where Self: Sized - { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(1), - Constraint::Min(1), - Constraint::Length(1), - ]) - .split(area); - - self.render_header(chunks[0], buf); - self.render_game_list(chunks[1], buf); - self.render_footer(chunks[2], buf); - - if let Some(boxed) = &mut self.state.popup && let Some(popup) = boxed.downcast_mut::() { - popup.clone().render(area, buf, popup); + if let Some(view) = self.state.view.as_mut() { + if let Some(main_view) = view.downcast_mut::() { + frame.render_stateful_widget(MainView::new(&self.app_config), frame.area(), &mut main_view.state); + } } } -} - -// render widgets -impl App { - fn render_game_list(&mut self, area: Rect, buf: &mut Buffer) { - let game_list = Block::new() - .title(Line::raw("Games")) - .borders(Borders::ALL) - .style(Style::default()); - game_list.render(area, buf); - } - - fn render_header(&mut self, area: Rect, buf: &mut Buffer) { - let title = Paragraph::new( - Text::styled( - "SuS Manager", - Style::default().fg(Color::Green), - ) - ); - title.render(area, buf); - } - - fn render_footer(&mut self, area: Rect, buf: &mut Buffer) { - let mut navigation_text = vec![ - Span::styled("(q) quit / (a) add folders", Style::default().fg(Color::Green)), - ]; - if matches!(self.status, AppStatus::Input) { - navigation_text[0] = Span::styled("Input Mode", Style::default().fg(Color::Green)); - } - let line = Line::from(navigation_text); - let footer = Paragraph::new(line); - footer.render(area, buf); - } -} - - -// event handlers -impl App { - fn quit(&mut self) -> Result<()> { - if self.state.popup.is_none() { - self.status = AppStatus::Exiting; - self.app_config - .clone() - .write_to_file(&APP_CONIFG_FILE_PATH.to_path_buf())?; - self.events.task.abort(); - } - else { - self.state.popup = None; - } - Ok(()) - } - - fn folder_popup(&mut self) { - self.state.popup = Some(Box::new(AddFolderPopup::new())); - self.status = AppStatus::Input; - } } \ No newline at end of file diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 7abe7c3..3d9b7ff 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -1,2 +1,3 @@ pub mod components; -pub mod popups; \ No newline at end of file +pub mod popups; +pub mod views; \ No newline at end of file diff --git a/src/widgets/popups/folder.rs b/src/widgets/popups/folder.rs index 5541158..4b61bd1 100644 --- a/src/widgets/popups/folder.rs +++ b/src/widgets/popups/folder.rs @@ -38,7 +38,7 @@ impl StatefulWidget for AddFolderPopup { let chunks = Layout::default() .direction(Direction::Vertical) .constraints(vec![ - Constraint::Length(1) + Constraint::Min(2) ]) .split(popup_area.inner(Margin::new(1, 1))); self.textarea.render(chunks[0], buf, &mut state.textarea); diff --git a/src/widgets/views/main_view.rs b/src/widgets/views/main_view.rs new file mode 100644 index 0000000..695f3a5 --- /dev/null +++ b/src/widgets/views/main_view.rs @@ -0,0 +1,138 @@ +use std::any::Any; +use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind}; +use crossterm::event::KeyCode::Char; +use ratatui::buffer::Buffer; +use ratatui::layout::{Constraint, Direction, Layout, Rect}; +use ratatui::prelude::{Color, Line, Span, Style, Text, Widget}; +use ratatui::widgets::{Block, Borders, Paragraph, StatefulWidget}; +use crate::config::types::ApplicationConfig; +use crate::constants::APP_CONIFG_FILE_PATH; +use crate::widgets::popups::folder::AddFolderPopup; +use crate::widgets::views::{AppStatus, View}; + +pub struct MainView { + app_config: ApplicationConfig, + pub state: MainViewState, +} + +#[derive(Debug)] +pub struct MainViewState { + popup: Option>, + status: AppStatus, +} + +impl MainView { + pub fn new(app_conf: &ApplicationConfig) -> Self { + Self { + state: MainViewState { + popup: None, + status: AppStatus::Running + }, + app_config: app_conf.clone(), + } + } + + fn quit(&mut self) -> color_eyre::Result<()> { + if self.state.popup.is_none() { + self.state.status = AppStatus::Exiting; + self.app_config + .clone() + .write_to_file(&APP_CONIFG_FILE_PATH.to_path_buf())?; + } + else { + self.state.popup = None; + } + Ok(()) + } + + fn folder_popup(&mut self) { + self.state.popup = Some(Box::new(AddFolderPopup::new())); + self.state.status = AppStatus::Input; + } +} + +impl View for MainView { + fn handle_input(&mut self, event: &Event) -> color_eyre::Result<()> { + if let Some(any) = self.state.popup.as_mut() && + let Some(popup) = any.downcast_mut::(){ + popup.textarea.handle_input(event)?; + } + Ok(()) + } + + fn handle_key_input(&mut self, key: &KeyEvent) -> color_eyre::Result<()> { + if matches!(self.state.status, AppStatus::Input) && matches!(key.code, KeyCode::Esc) { + self.state.status = AppStatus::Running; + } + if matches!(key.kind, KeyEventKind::Press) && !matches!(self.state.status, AppStatus::Input) { + match key.code { + Char('q') => self.quit()?, + Char('a') => self.folder_popup(), + _ => {} + } + } + Ok(()) + } + + fn is_running(&self) -> bool { + !matches!(self.state.status, AppStatus::Exiting) + } +} + +impl StatefulWidget for MainView { + type State = MainViewState; + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) + where + Self: Sized + { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(1), + Constraint::Min(1), + Constraint::Length(1), + ]) + .split(area); + + Self::render_header(state,chunks[0], buf); + Self::render_game_list(state,chunks[1], buf); + Self::render_footer(state,chunks[2], buf); + + if let Some(boxed) = state.popup.as_mut() && + let Some(popup) = boxed.downcast_mut::() { + popup.clone().render(area, buf, popup); + } + } +} + +impl MainView { + fn render_game_list(state: &mut MainViewState, area: Rect, buf: &mut Buffer) { + let game_list = Block::new() + .title(Line::raw("Games")) + .borders(Borders::ALL) + .style(Style::default()); + game_list.render(area, buf); + } + + fn render_header(state: &mut MainViewState, area: Rect, buf: &mut Buffer) { + let title = Paragraph::new( + Text::styled( + "SuS Manager", + Style::default().fg(Color::Green), + ) + ); + title.render(area, buf); + } + + fn render_footer(state: &mut MainViewState, area: Rect, buf: &mut Buffer) { + let mut navigation_text = vec![ + Span::styled("(q) quit / (a) add folders", Style::default().fg(Color::Green)), + ]; + if matches!(state.status, AppStatus::Input) { + navigation_text[0] = Span::styled("Input Mode", Style::default().fg(Color::Green)); + } + let line = Line::from(navigation_text); + let footer = Paragraph::new(line); + footer.render(area, buf); + } +} \ No newline at end of file diff --git a/src/widgets/views/mod.rs b/src/widgets/views/mod.rs new file mode 100644 index 0000000..5a9038f --- /dev/null +++ b/src/widgets/views/mod.rs @@ -0,0 +1,17 @@ +use std::any::Any; +use crossterm::event::{Event, KeyEvent}; + +pub mod main_view; + +#[derive(Debug, Clone, Copy)] +pub enum AppStatus { + Running, + Exiting, + Input +} + +pub trait View { + fn handle_input(&mut self, event: &Event) -> color_eyre::Result<()>; + fn handle_key_input(&mut self, key: &KeyEvent) -> color_eyre::Result<()>; + fn is_running(&self) -> bool; +} \ No newline at end of file