use gtk4 as gtk; use async_channel::{Receiver, Sender}; use chrono::{Datelike, NaiveDate, Weekday}; use gtk::gdk::Display; use gtk::glib; use gtk::glib::signal::Inhibit; use gtk::prelude::*; use std::future::Future; use std::rc::Rc; use crate::model::event::{Event, Time}; /// Spawns a task on the default executor, without waiting for it to complete pub fn spawn(future: F) where F: Future + 'static, { gtk::glib::MainContext::default().spawn_local(future); } fn send_message(tx: Sender, msg: Msg) { spawn(async move { let _ = tx.send(msg).await; }) } enum Msg { ShowAddForm { date: NaiveDate }, AddEvent { event: Event }, } 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 run(events: Vec) { let application = gtk::Application::new(Some("me.guyonvarch.calendar"), Default::default()); application.connect_startup(move |app| build_ui(app, &events)); application.run(); } fn build_ui(app: >k::Application, events: &Vec) { load_style(); let (tx, rx) = async_channel::unbounded(); let app = App::new(app, tx.clone(), &events); spawn(event_handler(rx, tx, app)) } async fn event_handler(rx: Receiver, tx: Sender, mut app: App) { while let Ok(msg) = rx.recv().await { match msg { Msg::ShowAddForm { date } => { spawn(add_event_dialog(tx.clone(), Rc::clone(&app.window), date)); } Msg::AddEvent { event } => { let date = event.date.clone(); let d = date.signed_duration_since(app.start_date).num_days(); app.events.push(event); let col = (d % 7) as i32; let row = 1 + (d / 7) as i32; app.grid.attach( &day_entry(tx.clone(), &date, &app.today, &app.events), col, row, 1, 1, ); } } } } struct App { pub window: Rc, pub grid: gtk::Grid, pub events: Vec, pub today: NaiveDate, pub start_date: NaiveDate, } impl App { fn new(app: >k::Application, tx: Sender, events: &Vec) -> Self { let window = Rc::new( gtk::ApplicationWindow::builder() .application(app) .title("Calendar") .default_width(800) .default_height(600) .visible(true) .build(), ); let grid = gtk::Grid::builder().build(); window.set_child(Some(&grid)); for col in 0..7 { grid.attach(&day_title(col), col, 0, 1, 1); } let today = chrono::offset::Local::today().naive_utc(); let start_date = NaiveDate::from_isoywd(today.year(), today.iso_week().week(), Weekday::Mon); show_days(tx, &grid, &start_date, &today, &events); window.connect_close_request(move |window| { if let Some(application) = window.application() { application.remove_window(window); } Inhibit(false) }); Self { window, grid, events: events.clone(), today, start_date, } } } fn load_style() { let provider = gtk::CssProvider::new(); provider.load_from_data(include_bytes!("style.css")); gtk::StyleContext::add_provider_for_display( &Display::default().expect("Error initializing gtk css provider."), &provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, ); } fn show_days( tx: Sender, grid: >k::Grid, start_day: &NaiveDate, today: &NaiveDate, events: &Vec, ) { let mut d = *start_day; for row in 1..5 { for col in 0..7 { grid.attach(&day_entry(tx.clone(), &d, &today, &events), col, row, 1, 1); d = d.succ(); } } } 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 } fn day_entry( tx: Sender, date: &NaiveDate, today: &NaiveDate, events: &Vec, ) -> 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 date => move |_, n, _, _| { if n == 2 { send_message(tx.clone(), Msg::ShowAddForm { date }); } })); vbox.add_controller(&gesture); if date == today { vbox.add_css_class("g-Calendar__Day--Today"); } vbox.append(&day_label(date)); let mut events = events .iter() .filter(|e| e.date == *date) .collect::>(); events.sort_by_key(|e| e.time); if !events.is_empty() { vbox.append(&day_events(events)); } let scrolled_window = gtk::ScrolledWindow::builder() .hscrollbar_policy(gtk::PolicyType::Never) .hexpand(true) .vexpand(true) .child(&vbox) .build(); scrolled_window } fn day_label(date: &NaiveDate) -> gtk::Label { let label = gtk::Label::builder() .label(&format!( "{} {}", date.day(), MONTHES[date.month0() as usize] )) .halign(gtk::Align::Start) .build(); label.add_css_class("g-Calendar__DayNumber"); label } fn day_events(events: Vec<&Event>) -> 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 gesture = gtk::GestureClick::new(); let click_event = event.clone(); gesture.connect_pressed(move |gesture, _, _, _| { gesture.set_state(gtk::EventSequenceState::Claimed); println!("Click: {:?}", click_event); }); hbox.add_controller(&gesture); hbox.add_css_class("g-Calendar__DayEvent"); 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 } async fn add_event_dialog(tx: Sender, window: Rc, date: NaiveDate) { let dialog = gtk::Dialog::builder() .transient_for(&*window) .modal(true) .title("Ajouter un évènement") .build(); let content_area = dialog.content_area(); let label = gtk::Label::builder().label(&format!("{:?}", date)).build(); content_area.append(&label); let entry = gtk::Entry::builder().build(); content_area.append(&entry); dialog.add_buttons(&[ ("Annuler", gtk::ResponseType::Cancel), ("Créer", gtk::ResponseType::Ok), ]); let answer = dialog.run_future().await; if answer == gtk::ResponseType::Ok { let event = Event { date, time: Time::AllDay, name: entry.buffer().text(), }; send_message(tx, Msg::AddEvent { event: event }); } dialog.close(); }