diff options
| -rw-r--r-- | Cargo.lock | 34 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | README.md | 12 | ||||
| -rw-r--r-- | src/app.rs | 296 | ||||
| -rw-r--r-- | src/main.rs | 165 | 
5 files changed, 353 insertions, 156 deletions
| @@ -9,6 +9,17 @@ 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"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -21,6 +32,12 @@ 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"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -48,9 +65,9 @@ dependencies = [  name = "calendar"  version = "0.1.0"  dependencies = [ + "async-channel",   "chrono",   "gtk4", - "pango",  ]  [[package]] @@ -85,12 +102,27 @@ dependencies = [  ]  [[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"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -7,4 +7,4 @@ edition = "2018"  [dependencies]  chrono = "0.4"  gtk4 = { version = "0.3", features = ["v4_2"] } -pango = "0.14" +async-channel = "1.6" @@ -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<F>(future: F) +where +    F: Future<Output = ()> + 'static, +{ +    gtk::glib::MainContext::default().spawn_local(future); +} + +fn send_message(tx: Sender<Msg>, 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<Event>) { +    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<Event>) { +    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<Msg>, tx: Sender<Msg>, 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<gtk::ApplicationWindow>, +    pub grid: gtk::Grid, +    pub events: Vec<Event>, +    pub today: NaiveDate, +    pub start_date: NaiveDate, +} + +impl App { +    fn new(app: >k::Application, tx: Sender<Msg>, events: &Vec<Event>) -> 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<Msg>, +    grid: >k::Grid, +    start_day: &NaiveDate, +    today: &NaiveDate, +    events: &Vec<Event>, +) { +    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<Msg>, +    date: &NaiveDate, +    today: &NaiveDate, +    events: &Vec<Event>, +) -> 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::<Vec<&Event>>(); +    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<Msg>, window: Rc<gtk::ApplicationWindow>, 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> { +    [          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<Event>) { -    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<Event>) -> 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::<Vec<&Event>>(); - -    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()  } | 
