use crate::config::types::ApplicationConfig; use crate::constants::APP_CONIFG_FILE_PATH; use crate::widgets::popups::folder::AddFolderPopup; use crate::widgets::views::View; use crossterm::event::KeyCode::Char; use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind}; use rat_cursor::HasScreenCursor; 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 std::any::Any; pub struct MainView { app_config: ApplicationConfig, pub state: MainViewState, } #[derive(Debug)] pub struct MainViewState { popup: Option>, status: Status, } #[derive(Debug, Clone, Copy)] enum Status { Running, Exiting, Popup, } impl MainView { pub fn new(app_conf: &ApplicationConfig) -> Self { Self { state: MainViewState { popup: None, status: Status::Running, }, app_config: app_conf.clone(), } } fn quit(&mut self) -> color_eyre::Result<()> { if self.state.popup.is_none() { self.state.status = Status::Exiting; self.app_config .clone() .write_to_file(&APP_CONIFG_FILE_PATH.to_path_buf())?; } Ok(()) } fn folder_popup(&mut self) { self.state.popup = Some(Box::new(AddFolderPopup::new())); self.state.status = Status::Popup; } } 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, Status::Popup) && matches!(key.code, KeyCode::Esc) { self.state.status = Status::Running; self.state.popup = None; } if !matches!(self.state.status, Status::Popup) && matches!(key.kind, KeyEventKind::Press) { match key.code { Char('q') => self.quit()?, Char('a') => self.folder_popup(), _ => {} } } Ok(()) } fn is_running(&self) -> bool { !matches!(self.state.status, Status::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(chunks[0], buf); Self::render_game_list(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 HasScreenCursor for MainView { fn screen_cursor(&self) -> Option<(u16, u16)> { if let Some(popup) = &self.state.popup && let Some(add_folder) = popup.downcast_ref::() { return add_folder.textarea.screen_cursor(); } None } } impl MainView { fn render_game_list(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(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, Status::Popup) { navigation_text[0] = Span::styled("(Esc) close", Style::default().fg(Color::Green)); } let line = Line::from(navigation_text); let footer = Paragraph::new(line); footer.render(area, buf); } }