use gtk4 as gtk; use async_channel::Sender; use chrono::{Datelike, NaiveDate}; use gtk::glib; use gtk::prelude::*; use std::collections::HashMap; use crate::{gui::update, gui::update::Msg, gui::App, model::event, model::event::Event, model::category::Category}; static DAYS: [&str; 7] = ["LUN", "MAR", "MER", "JEU", "VEN", "SAM", "DIM"]; static MONTHES: [&str; 12] = [ "Jan", "Fév", "Mar", "Avr", "Mai", "Juin", "Juil", "Aoû", "Sep", "Oct", "Nov", "Déc", ]; pub fn create( tx: Sender, today: NaiveDate, start_date: NaiveDate, end_date: NaiveDate, events: &[Event], recurring_events: &[Event], categories: &[Category], default_color: &str, ) -> gtk::Grid { let grid = gtk::Grid::builder().build(); for col in 0..7 { grid.attach(&day_title(col), col, 0, 1, 1); } let repetitions = event::repetitions_between(recurring_events, start_date, end_date); attach_days(tx.clone(), &grid, start_date, today, events, &repetitions, categories, default_color); let event_controller_key = gtk::EventControllerKey::new(); event_controller_key.connect_key_released(glib::clone!(@strong tx => move |_, _, keycode, _| { match keycode { 111 => update::send(tx.clone(), Msg::SelectPreviousWeek), // UP 116 => update::send(tx.clone(), Msg::SelectNextWeek), // DOWN _ => () } })); grid.add_controller(&event_controller_key); grid } fn attach_days( tx: Sender, grid: >k::Grid, start_date: NaiveDate, today: NaiveDate, events: &[Event], repetitions: &HashMap>, categories: &[Category], default_color: &str, ) { let mut d = start_date; for row in 1..5 { for col in 0..7 { grid.attach( &day_entry(tx.clone(), d, today, events, repetitions, categories, default_color), col, row, 1, 1, ); // TODO: error handling d = d.succ_opt().unwrap(); } } } pub fn refresh_date( app: &App, date: NaiveDate, repetitions: &HashMap>, categories: &[Category], default_color: &str ) { let d = date.signed_duration_since(app.start_date).num_days(); let col = (d % 7) as i32; let row = 1 + (d / 7) as i32; app.calendar.attach( &day_entry(app.tx.clone(), date, app.today, &app.events, repetitions, categories, default_color), col, row, 1, 1, ) } fn day_title(col: i32) -> gtk::Box { let vbox = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .build(); vbox.add_css_class("g-Calendar__DayTitle"); let label = gtk::Label::builder().label(DAYS[col as usize]).build(); vbox.append(&label); vbox } pub fn day_entry( tx: Sender, date: NaiveDate, today: NaiveDate, events: &[Event], repetitions: &HashMap>, categories: &[Category], default_color: &str, ) -> gtk::ScrolledWindow { let vbox = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .build(); vbox.add_css_class("g-Calendar__Day"); let gesture = gtk::GestureClick::new(); gesture.connect_pressed(glib::clone!(@strong tx => move |_, n, _, _| { if n == 2 { update::send(tx.clone(), Msg::ShowAddForm { date }); } })); vbox.add_controller(&gesture); if date == today { vbox.add_css_class("g-Calendar__Day--Today"); } vbox.append(&day_label(today, date)); let mut events = events .iter() .filter(|e| e.date == date) .collect::>(); let recurring_events = repetitions.get(&date).cloned().unwrap_or_default(); events.extend(recurring_events.iter()); events.sort_by_key(|e| e.start); if !events.is_empty() { vbox.append(&day_events(date, tx, events, categories, default_color)); } gtk::ScrolledWindow::builder() .hscrollbar_policy(gtk::PolicyType::Never) .hexpand(true) .vexpand(true) .child(&vbox) .build() } fn day_label(today: NaiveDate, date: NaiveDate) -> gtk::Label { let label = gtk::Label::builder() .label(&format!( "{} {}", date.day(), if date == today || date.day() == 1 { MONTHES[date.month0() as usize] } else { "" } )) .halign(gtk::Align::Start) .build(); label.add_css_class("g-Calendar__DayNumber"); if date.day() == 1 { label.add_css_class("g-Calendar__DayNumber--FirstOfMonth") } label } fn day_events( date: NaiveDate, tx: Sender, events: Vec<&Event>, categories: &[Category], default_color: &str, ) -> gtk::Box { let vbox = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .build(); for event in events { let hbox = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .hexpand(true) .build(); let background_color = categories.iter().find(|c| event.category == Some(c.id)).map(|c| c.color.clone()).unwrap_or_else(|| default_color.to_string()); let provider = gtk::CssProvider::new(); provider.load_from_data(format!(" .event {{ background-color: {}; color: white; border-radius: 4px; padding: 4px; margin: 4px; }} .event:hover {{ filter: brightness(120%); }} ", background_color).as_bytes()); hbox.style_context().add_provider(&provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION); hbox.style_context().add_class("event"); let gesture = gtk::GestureClick::new(); gesture.connect_pressed( glib::clone!(@strong event, @strong tx => move |gesture, n, _, _| { gesture.set_state(gtk::EventSequenceState::Claimed); if n == 2 { if event.repetition.is_some() { update::send(tx.clone(), Msg::ShowRepetitionDialog { date, event_id: event.id }); } else { update::send(tx.clone(), Msg::ShowUpdateForm { event_id: event.id }); } } }), ); hbox.add_controller(&gesture); let event_txt = &event.pprint(); let label = gtk::Label::builder() .label(event_txt) .ellipsize(gtk::pango::EllipsizeMode::End) .tooltip_text(event_txt) .halign(gtk::Align::Start) .build(); hbox.append(&label); vbox.append(&hbox); } vbox }