From 99af88a840bef534540a4b273d24a8a17e7fc9b9 Mon Sep 17 00:00:00 2001 From: Joris Date: Sun, 9 Jan 2022 13:12:45 +0100 Subject: Split app into modules --- src/app/app.rs | 57 +++++++++++++++++++ src/app/calendar.rs | 157 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/app/form.rs | 79 ++++++++++++++++++++++++++ src/app/mod.rs | 38 +++++++++++++ src/app/style.css | 45 +++++++++++++++ src/app/update.rs | 52 +++++++++++++++++ src/app/utils.rs | 9 +++ 7 files changed, 437 insertions(+) create mode 100644 src/app/app.rs create mode 100644 src/app/calendar.rs create mode 100644 src/app/form.rs create mode 100644 src/app/mod.rs create mode 100644 src/app/style.css create mode 100644 src/app/update.rs create mode 100644 src/app/utils.rs (limited to 'src/app') diff --git a/src/app/app.rs b/src/app/app.rs new file mode 100644 index 0000000..45904a9 --- /dev/null +++ b/src/app/app.rs @@ -0,0 +1,57 @@ +use gtk4 as gtk; + +use async_channel::Sender; +use chrono::{Datelike, NaiveDate, Weekday}; +use gtk::glib::signal::Inhibit; +use gtk::prelude::*; +use rusqlite::Connection; +use std::rc::Rc; + +use crate::app::calendar; +use crate::app::update::Msg; +use crate::{db, model::event::Event}; + +pub struct App { + pub window: Rc, + pub grid: gtk::Grid, + pub events: Vec, + pub today: NaiveDate, + pub start_date: NaiveDate, +} + +impl App { + pub fn new(conn: Rc, app: >k::Application, tx: Sender) -> Self { + let window = Rc::new( + gtk::ApplicationWindow::builder() + .application(app) + .title("Calendar") + .default_width(800) + .default_height(600) + .visible(true) + .build(), + ); + + let today = chrono::offset::Local::today().naive_utc(); + let start_date = + NaiveDate::from_isoywd(today.year(), today.iso_week().week(), Weekday::Mon); + let events = db::list(&conn).unwrap_or(vec![]); + let grid = calendar::grid(tx, &today, &start_date, &events); + + window.set_child(Some(&grid)); + + window.connect_close_request(move |window| { + if let Some(application) = window.application() { + application.remove_window(window); + } + Inhibit(false) + }); + + Self { + window, + grid, + events, + today, + start_date, + } + } +} diff --git a/src/app/calendar.rs b/src/app/calendar.rs new file mode 100644 index 0000000..847ea71 --- /dev/null +++ b/src/app/calendar.rs @@ -0,0 +1,157 @@ +use gtk4 as gtk; + +use async_channel::Sender; +use chrono::{Datelike, NaiveDate}; +use gtk::glib; +use gtk::prelude::*; + +use crate::{app::update, app::update::Msg, model::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 grid( + tx: Sender, + today: &NaiveDate, + start_date: &NaiveDate, + events: &Vec, +) -> gtk::Grid { + let grid = gtk::Grid::builder().build(); + + for col in 0..7 { + grid.attach(&day_title(col), col, 0, 1, 1); + } + + show_days(tx, &grid, &start_date, &today, &events); + + grid +} + +fn show_days( + tx: Sender, + grid: >k::Grid, + start_date: &NaiveDate, + today: &NaiveDate, + events: &Vec, +) { + let mut d = *start_date; + 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 +} + +pub 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 { + update::send(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.start); + + 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 +} diff --git a/src/app/form.rs b/src/app/form.rs new file mode 100644 index 0000000..fc3dc83 --- /dev/null +++ b/src/app/form.rs @@ -0,0 +1,79 @@ +use gtk4 as gtk; + +use async_channel::Sender; +use chrono::NaiveDate; +use gtk::glib; +use gtk::prelude::*; +use rusqlite::Connection; +use std::rc::Rc; + +use crate::{app::update, app::update::Msg, db, model::event}; + +pub async fn dialog( + conn: Rc, + tx: Sender, + window: Rc, + date: NaiveDate, +) { + let dialog = gtk::Dialog::builder() + .transient_for(&*window) + .modal(true) + .title("Ajouter") + .css_classes(vec!["g-Form".to_string()]) + .build(); + + let content_area = dialog.content_area(); + + let vbox = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .build(); + vbox.add_css_class("g-Form__Inputs"); + content_area.append(&vbox); + + let name = entry(""); + vbox.append(&label("Événement")); + vbox.append(&name); + + let date = entry(&date.format(event::DATE_FORMAT).to_string()); + vbox.append(&label("Jour")); + vbox.append(&date); + + let start = entry(""); + vbox.append(&label("Début")); + vbox.append(&start); + + let end = entry(""); + vbox.append(&label("Fin")); + vbox.append(&end); + + let button = gtk::Button::with_label("Créer"); + vbox.append(&button); + button.connect_clicked(glib::clone!(@weak dialog => move |_| { + match event::validate(date.buffer().text(), name.buffer().text(), start.buffer().text(), end.buffer().text()) { + Some(event) => { + match db::insert(&conn, &event) { + Ok(_) => { + update::send(tx.clone(), Msg::AddEvent { event: event }); + dialog.close() + }, + Err(_) => () + } + }, + None => () + } + })); + + dialog.run_future().await; +} + +fn entry(text: &str) -> gtk::Entry { + gtk::Entry::builder().text(text).margin_bottom(10).build() +} + +fn label(text: &str) -> gtk::Label { + gtk::Label::builder() + .label(text) + .halign(gtk::Align::Start) + .margin_bottom(5) + .build() +} diff --git a/src/app/mod.rs b/src/app/mod.rs new file mode 100644 index 0000000..30b59af --- /dev/null +++ b/src/app/mod.rs @@ -0,0 +1,38 @@ +mod app; +mod calendar; +mod form; +mod update; +mod utils; + +use gtk4 as gtk; + +use gtk::gdk::Display; +use gtk::prelude::*; +use rusqlite::Connection; +use std::rc::Rc; + +use app::App; + +pub fn run(conn: Connection) { + let conn = Rc::new(conn); + let app = gtk::Application::new(Some("me.guyonvarch.calendar"), Default::default()); + app.connect_startup(|_| load_style()); + app.connect_activate(move |app| build_ui(conn.clone(), app)); + app.run(); +} + +fn build_ui(conn: Rc, app: >k::Application) { + let (tx, rx) = async_channel::unbounded(); + let app = App::new(conn.clone(), app, tx.clone()); + utils::spawn(update::event_handler(conn, rx, tx, app)) +} + +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, + ); +} diff --git a/src/app/style.css b/src/app/style.css new file mode 100644 index 0000000..5cd1394 --- /dev/null +++ b/src/app/style.css @@ -0,0 +1,45 @@ +.g-Calendar__DayTitle { + border-right: 1px solid #D2D2D2; + border-bottom: 1px solid #D2D2D2; + padding: 4px; + background-color: white; + font-weight: bold; +} + +.g-Calendar__Day { + background-color: white; + border-right: 1px solid #D2D2D2; + border-bottom: 1px solid #D2D2D2; + padding: 4px; +} + +.g-Calendar__Day--Today { + background-color: #FFFCD8; +} + +.g-Calendar__DayNumber { + font-size: 90%; + margin-bottom: 4px; +} + +.g-Calendar__DayEvent { + background-color: #A8C2E0; + color: white; + border-radius: 4px; + padding: 4px; + margin: 4px; +} + +.g-Calendar__DayEvent:hover { + background-color: pink; +} + +.g-Form { + background-color: white; + color: black; + padding: 10px; +} + +.g-Form__Input { + text-align: left; +} diff --git a/src/app/update.rs b/src/app/update.rs new file mode 100644 index 0000000..288ec51 --- /dev/null +++ b/src/app/update.rs @@ -0,0 +1,52 @@ +use gtk4 as gtk; + +use async_channel::{Receiver, Sender}; +use chrono::NaiveDate; +use gtk::prelude::*; +use rusqlite::Connection; +use std::rc::Rc; + +use crate::app::calendar; +use crate::app::form; +use crate::app::utils; +use crate::app::App; +use crate::model::event::Event; + +pub fn send(tx: Sender, msg: Msg) { + utils::spawn(async move { + let _ = tx.send(msg).await; + }) +} + +pub enum Msg { + ShowAddForm { date: NaiveDate }, + AddEvent { event: Event }, +} + +pub async fn event_handler(conn: Rc, rx: Receiver, tx: Sender, mut app: App) { + while let Ok(msg) = rx.recv().await { + match msg { + Msg::ShowAddForm { date } => { + form::dialog(Rc::clone(&conn), tx.clone(), Rc::clone(&app.window), date).await; + } + 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( + &calendar::day_entry(tx.clone(), &date, &app.today, &app.events), + col, + row, + 1, + 1, + ); + } + } + } +} diff --git a/src/app/utils.rs b/src/app/utils.rs new file mode 100644 index 0000000..673b96e --- /dev/null +++ b/src/app/utils.rs @@ -0,0 +1,9 @@ +use std::future::Future; + +/// Spawns a task on the default executor, without waiting for it to complete +pub fn spawn(future: F) +where + F: Future + 'static, +{ + gtk4::glib::MainContext::default().spawn_local(future); +} -- cgit v1.2.3