use std::io; use chrono::{Datelike, Local}; use ratatui::{ DefaultTerminal, Frame, crossterm::event::{self, KeyEvent, KeyEventKind}, layout::{Constraint, Direction, Layout, Rect}, }; use crate::{ database::DB, days::Days, events::AppEvent, focused::Focused, month::Month, popup::Popup, year::Year, }; #[derive(Debug, Clone, Copy, PartialEq)] pub enum FocusedComponent { Year, Month, Days, DayPopup, } pub struct App<'a> { year: Year, month: Month, days: Days, day_popup: Popup<'a>, focused: FocusedComponent, exit: bool, } impl App<'_> { pub fn new() -> Self { let mut app = App { year: Year::default(), month: Month::default(), days: Days::default(), day_popup: Popup::default(), focused: FocusedComponent::Days, exit: false, }; app.days .reload(app.month.month, app.year.year, Local::now().day() as i32); app.get_focused_mut().take_focus(); app } fn get_focused_mut(&mut self) -> &mut dyn Focused { match self.focused { FocusedComponent::Year => &mut self.year, FocusedComponent::Month => &mut self.month, FocusedComponent::Days => &mut self.days, FocusedComponent::DayPopup => &mut self.day_popup, } } pub fn switch_focus(&mut self, new_focus: FocusedComponent) { if self.focused != new_focus { self.get_focused_mut().lose_focus(); self.focused = new_focus; self.get_focused_mut().take_focus(); } } fn handle_input(&mut self, key_event: KeyEvent, db: &mut DB) -> Option> { let focused = self.get_focused_mut(); focused.handle_input(key_event, db) } pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> { let mut db = DB::new().unwrap(); while !self.exit { terminal.draw(|frame| self.draw(frame, &mut db))?; self.handle_events(&mut db)?; } Ok(()) } fn draw(&mut self, frame: &mut Frame, db: &mut DB) { let main_area = Layout::default() .direction(Direction::Vertical) .constraints(vec![ Constraint::Length(8), Constraint::Length(4), Constraint::Fill(1), ]) .split(frame.area()); let popup_area = Self::popup_rect(50, 50, frame.area()); if frame.area().width < 10 || frame.area().height < 30 { self.exit(); return; } self.year.ready_to_render(main_area[0]); self.month.ready_to_render(main_area[1]); self.days.ready_to_render(main_area[2], db); self.day_popup.ready_to_render(popup_area); frame.render_widget(&self.year, main_area[0]); frame.render_widget(&self.month, main_area[1]); frame.render_widget(&self.days, main_area[2]); frame.render_widget(&self.day_popup, popup_area); } fn popup_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { let popup_layout = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Percentage((100 - percent_y) / 2), Constraint::Percentage(percent_y), Constraint::Percentage((100 - percent_y) / 2), ]) .split(r); Layout::default() .direction(Direction::Horizontal) .constraints([ Constraint::Percentage((100 - percent_x) / 2), Constraint::Percentage(percent_x), Constraint::Percentage((100 - percent_x) / 2), ]) .split(popup_layout[1])[1] } fn exit(&mut self) { self.exit = true; } fn handle_events(&mut self, db: &mut DB) -> io::Result<()> { match event::read()? { ratatui::crossterm::event::Event::Key(key_event) if key_event.kind == KeyEventKind::Press => { let app_event_option = self.handle_input(key_event, db); if let Some(app_event_vec) = app_event_option { for app_event in app_event_vec { match app_event { AppEvent::SwitchFocus(new_focus) => self.switch_focus(new_focus), AppEvent::Exit => self.exit(), AppEvent::YearScrolled(dir) => match dir { crate::events::Direction::Up => { self.year.plus_year(); } crate::events::Direction::Down => { self.year.minus_year(); } }, AppEvent::MonthScrolled(dir) => match dir { crate::events::Direction::Up => { self.month.plus_month(); } crate::events::Direction::Down => { self.month.minus_month(); } }, AppEvent::Reload => { self.days.reload_day_counts(db); } AppEvent::MonthSet(month) => { self.days.reload(month, self.year.year, 1); } AppEvent::YearSet(year) => { self.days.reload(self.month.month, year, 1); } AppEvent::AddEvent(start, end) => { self.switch_focus(FocusedComponent::DayPopup); self.day_popup.add_event( start, end, db.get_tags().expect("Database error"), ); } AppEvent::DaySelected(day) => { self.switch_focus(FocusedComponent::DayPopup); let events = db.get_day(day).expect("Database error").unwrap_or_default(); let tag_names = events .iter() .map(|e| match e.tag { Some(id) => { db.get_tag_name(id).expect("Database error").unwrap() } None => "Tag deleted".to_owned(), }) .collect(); self.day_popup.show_day(day, events, tag_names); } } } } } _ => {} }; Ok(()) } }