calendar/src/app.rs
2025-09-24 22:24:50 +02:00

207 lines
7.1 KiB
Rust

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<Vec<AppEvent>> {
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(())
}
}