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 --- Cargo.lock | 34 ++++++- Cargo.toml | 2 +- README.md | 12 ++- src/app.rs | 296 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 165 +++------------------------------ 5 files changed, 353 insertions(+), 156 deletions(-) create mode 100644 src/app.rs diff --git a/Cargo.lock b/Cargo.lock index c25214f..a31031f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,17 @@ version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee10e43ae4a853c0a3591d4e2ada1719e553be18199d9da9d4a83f5927c2f5c7" +[[package]] +name = "async-channel" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + [[package]] name = "autocfg" version = "1.0.1" @@ -20,6 +31,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "cache-padded" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba" + [[package]] name = "cairo-rs" version = "0.14.9" @@ -48,9 +65,9 @@ dependencies = [ name = "calendar" version = "0.1.0" dependencies = [ + "async-channel", "chrono", "gtk4", - "pango", ] [[package]] @@ -84,12 +101,27 @@ dependencies = [ "winapi", ] +[[package]] +name = "concurrent-queue" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +dependencies = [ + "cache-padded", +] + [[package]] name = "either" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "event-listener" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59" + [[package]] name = "field-offset" version = "0.3.4" diff --git a/Cargo.toml b/Cargo.toml index f240486..277ac45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,4 +7,4 @@ edition = "2018" [dependencies] chrono = "0.4" gtk4 = { version = "0.3", features = ["v4_2"] } -pango = "0.14" +async-channel = "1.6" diff --git a/README.md b/README.md index b6bfd91..3eb537b 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,16 @@ nix develop --command cargo run # Links -- gtk4 documentation: https://gtk-rs.org/gtk4-rs/stable/latest/docs/gtk4/ +- gtk4-rs book: https://gtk-rs.org/gtk4-rs/git/book/ +- gtk4-rs documentation: https://gtk-rs.org/gtk4-rs/stable/latest/docs/gtk4/ +- Event-Driven GTK by Example — 2021 Edition: https://mmstick.github.io/gtkrs-tutorials/ # TODO ## CRUD -1. Show a form popup when double clicking on a day. -2. Save to DB and update the calendar on validating. +1. Complete dialog form. +2. Save to DB 3. Read events from DB on startup. 4. Modify an event when double clicking. 5. Delete an event (Right click > Delete). @@ -29,7 +31,8 @@ Be able to specify repetition. ## API -1. Get list of today events. +1. Give DB path with CLI arg. +2. Get list of today events. ## Calendar focus @@ -47,3 +50,4 @@ Be able to specify repetition. - Drag & drop events. - Show an indicator when a day can be scrolled vertically. - Multi day events +- Prevent to launch multiple instances 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(); +} diff --git a/src/main.rs b/src/main.rs index 557f8ee..9dbed82 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,32 +1,29 @@ +mod app; mod model; -use gtk4 as gtk; - -use chrono::{Datelike, NaiveDate, NaiveTime, Weekday}; -use gtk::gdk::Display; -use gtk::prelude::*; +use chrono::{NaiveDate, NaiveTime}; use crate::model::event::{Event, Time}; -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", -]; - fn main() { - let mut events = [ + let events = test_events(); + app::run(events) +} + +fn test_events() -> Vec { + [ Event { - date: NaiveDate::from_ymd(2021, 11, 22), + date: NaiveDate::from_ymd(2021, 11, 29), time: Time::AllDay, name: "Début de la semaine".to_string(), }, Event { - date: NaiveDate::from_ymd(2021, 11, 26), + date: NaiveDate::from_ymd(2021, 12, 4), time: Time::AllDay, name: "Fin de la semaine".to_string(), }, Event { - date: NaiveDate::from_ymd(2021, 11, 26), + date: NaiveDate::from_ymd(2021, 12, 4), time: Time::Time { start: NaiveTime::from_hms(15, 0, 0), end: Some(NaiveTime::from_hms(15, 30, 0)), @@ -34,7 +31,7 @@ fn main() { name: "Appel".to_string(), }, Event { - date: NaiveDate::from_ymd(2021, 11, 26), + date: NaiveDate::from_ymd(2021, 12, 4), time: Time::Time { start: NaiveTime::from_hms(12, 0, 0), end: Some(NaiveTime::from_hms(14, 0, 0)), @@ -42,7 +39,7 @@ fn main() { name: "Repas".to_string(), }, Event { - date: NaiveDate::from_ymd(2021, 11, 26), + date: NaiveDate::from_ymd(2021, 12, 4), time: Time::Time { start: NaiveTime::from_hms(8, 0, 0), end: None, @@ -50,7 +47,7 @@ fn main() { name: "Promener le chien".to_string(), }, Event { - date: NaiveDate::from_ymd(2021, 11, 26), + date: NaiveDate::from_ymd(2021, 12, 4), time: Time::Time { start: NaiveTime::from_hms(9, 0, 0), end: None, @@ -58,137 +55,5 @@ fn main() { name: "Thé".to_string(), }, ] - .to_vec(); - events.sort_by_key(|e| e.time); - 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) { - let window = gtk::ApplicationWindow::new(app); - window.set_title(Some("Calendar")); - window.set_default_size(800, 600); - - load_style(); - - 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 last_monday = NaiveDate::from_isoywd(today.year(), today.iso_week().week(), Weekday::Mon); - - let mut d = last_monday; - for row in 1..5 { - for col in 0..7 { - grid.attach(&day_entry(&d, &today, &events), col, row, 1, 1); - d = d.succ(); - } - } - - window.show(); -} - -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 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(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"); - - if date == today { - vbox.add_css_class("g-Calendar__Day--Today"); - } - - vbox.append(&day_label(date)); - - let events = events - .iter() - .filter(|e| e.date == *date) - .collect::>(); - - 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(); - - hbox.add_css_class("g-Calendar__DayEvent"); - - let event_txt = &event.pprint(); - let label = gtk::Label::builder() - .label(&event_txt) - .ellipsize(pango::EllipsizeMode::End) - .tooltip_text(&event_txt) - .halign(gtk::Align::Start) - .build(); - - hbox.append(&label); - vbox.append(&hbox); - } - - vbox + .to_vec() } -- cgit v1.2.3