aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock34
-rw-r--r--Cargo.toml2
-rw-r--r--README.md12
-rw-r--r--src/app.rs296
-rw-r--r--src/main.rs165
5 files changed, 353 insertions, 156 deletions
diff --git a/Cargo.lock b/Cargo.lock
index c25214f..a31031f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
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<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: &gtk::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: &gtk::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: &gtk::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: &gtk::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()
}