From ce978143f1360e16e85587644055a9f83d11c64c Mon Sep 17 00:00:00 2001 From: Joris Date: Sun, 28 Nov 2021 18:51:44 +0100 Subject: Allow to add events --- src/app.rs | 296 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 src/app.rs (limited to 'src/app.rs') diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..5717c12 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,296 @@ +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(); +} -- cgit v1.2.3