From d70ca08db21d9743e2d3a3ad237ef8a61e33fc53 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 19 Feb 2024 16:47:10 -0500 Subject: [PATCH 1/5] Correctly set the date range picker when initializing the historical view --- fitnesstrax/app/src/components/date_range.rs | 4 ++-- fitnesstrax/app/src/views/historical_view.rs | 17 ++++++++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/fitnesstrax/app/src/components/date_range.rs b/fitnesstrax/app/src/components/date_range.rs index 6cb0198..8f014bd 100644 --- a/fitnesstrax/app/src/components/date_range.rs +++ b/fitnesstrax/app/src/components/date_range.rs @@ -65,12 +65,12 @@ impl DateRangePicker { *self.imp().on_search.borrow_mut() = Box::new(f); } - fn set_interval(&self, start: chrono::NaiveDate, end: chrono::NaiveDate) { + pub fn set_interval(&self, start: chrono::NaiveDate, end: chrono::NaiveDate) { self.imp().start.set_date(start); self.imp().end.set_date(end); } - fn interval(&self) -> DayInterval { + pub fn interval(&self) -> DayInterval { DayInterval { start: self.imp().start.date(), end: self.imp().end.date(), diff --git a/fitnesstrax/app/src/views/historical_view.rs b/fitnesstrax/app/src/views/historical_view.rs index 02848ee..ec7b384 100644 --- a/fitnesstrax/app/src/views/historical_view.rs +++ b/fitnesstrax/app/src/views/historical_view.rs @@ -30,6 +30,7 @@ use std::{cell::RefCell, rc::Rc}; pub struct HistoricalViewPrivate { app: Rc>>, list_view: gtk::ListView, + date_range_picker: DateRangePicker, } #[glib::object_subclass] @@ -47,12 +48,15 @@ impl ObjectSubclass for HistoricalViewPrivate { .set_child(Some(&DaySummary::new())); }); + let date_range_picker = DateRangePicker::default(); + let s = Self { app: Rc::new(RefCell::new(None)), list_view: gtk::ListView::builder() .factory(&factory) .single_click_activate(true) .build(), + date_range_picker, }; factory.connect_bind({ @@ -106,17 +110,11 @@ impl HistoricalView { *s.imp().app.borrow_mut() = Some(app); - let date_range_picker = DateRangePicker::default(); - date_range_picker.connect_on_search({ + s.imp().date_range_picker.connect_on_search({ let s = s.clone(); move |interval| s.set_interval(interval) }); - - let mut model = gio::ListStore::new::(); - model.extend(interval.days().map(Date::new)); - s.imp() - .list_view - .set_model(Some(>k::NoSelection::new(Some(model)))); + s.set_interval(interval); s.imp().list_view.connect_activate({ let on_select_day = on_select_day.clone(); @@ -135,7 +133,7 @@ impl HistoricalView { .hscrollbar_policy(gtk::PolicyType::Never) .build(); - s.append(&date_range_picker); + s.append(&s.imp().date_range_picker); s.append(&scroller); s @@ -147,6 +145,7 @@ impl HistoricalView { self.imp() .list_view .set_model(Some(>k::NoSelection::new(Some(model)))); + self.imp().date_range_picker.set_interval(interval.start, interval.end); } } -- 2.44.1 From c24a5f515f33c88501cf10c2e2ec58067212f8a1 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 19 Feb 2024 17:27:02 -0500 Subject: [PATCH 2/5] Set up CSS styles around the date range picker --- fitnesstrax/app/resources/style.css | 43 ++++++++++++++++- fitnesstrax/app/src/components/date_field.rs | 7 +-- fitnesstrax/app/src/components/date_range.rs | 49 ++++++++++++++------ fitnesstrax/app/src/views/historical_view.rs | 5 +- 4 files changed, 83 insertions(+), 21 deletions(-) diff --git a/fitnesstrax/app/resources/style.css b/fitnesstrax/app/resources/style.css index b375633..696a917 100644 --- a/fitnesstrax/app/resources/style.css +++ b/fitnesstrax/app/resources/style.css @@ -11,13 +11,52 @@ padding: 8px; } -.welcome__footer {} - .historical { margin: 32px; border-radius: 8px; } +.date-range-picker { + margin-bottom: 16px; +} + +/* +.date-range-picker > box:not(:last-child) { + margin-bottom: 8px; +} +*/ + +.date-range-picker__date-field { + margin: 8px; + font-size: x-large; +} + +.date-range-picker__search-button { + margin: 8px; + font-size: x-large; +} + +.date-range-picker__range-button { + margin: 8px; + font-size: x-large; +} + +.date-field > label { + font-size: x-large; +} + +.date-field__year { + margin: 0px 4px 0px 0px; +} + +.date-field__month { + margin: 0px 4px 0px 4px; +} + +.date-field__day { + margin: 0px 0px 0px 4px; +} + .day-summary { padding: 8px; } diff --git a/fitnesstrax/app/src/components/date_field.rs b/fitnesstrax/app/src/components/date_field.rs index 52ffccf..fc6a80a 100644 --- a/fitnesstrax/app/src/components/date_field.rs +++ b/fitnesstrax/app/src/components/date_field.rs @@ -113,11 +113,12 @@ glib::wrapper! { impl DateField { pub fn new(date: chrono::NaiveDate) -> Self { let s: Self = Object::builder().build(); - - println!("{}", date); + s.add_css_class("date-field"); s.append(&s.imp().year.widget()); + s.append(>k::Label::new(Some("-"))); s.append(&s.imp().month.widget()); + s.append(>k::Label::new(Some("-"))); s.append(&s.imp().day.widget()); s.set_date(date); @@ -146,7 +147,7 @@ impl DateField { #[cfg(test)] mod test { use super::*; - use crate::gtk_init::gtk_init; + // use crate::gtk_init::gtk_init; // Enabling this test pushes tests on the TextField into an infinite loop. That likely indicates a bad interaction within the TextField itself, and that is going to need to be fixed. #[test] diff --git a/fitnesstrax/app/src/components/date_range.rs b/fitnesstrax/app/src/components/date_range.rs index 8f014bd..fe971d5 100644 --- a/fitnesstrax/app/src/components/date_range.rs +++ b/fitnesstrax/app/src/components/date_range.rs @@ -14,14 +14,11 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see . */ -use crate::{ - components::{DateField}, - types::DayInterval, -}; +use crate::{components::DateField, types::DayInterval}; use chrono::{Duration, Local, Months}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; -use std::{cell::RefCell}; +use std::cell::RefCell; type OnSearch = dyn Fn(DayInterval) + 'static; @@ -40,9 +37,14 @@ impl ObjectSubclass for DateRangePickerPrivate { fn new() -> Self { let default_date = Local::now().date_naive(); + let start = DateField::new(default_date); + start.add_css_class("date-range-picker__date-field"); + let end = DateField::new(default_date); + end.add_css_class("date-range-picker__date-field"); + Self { - start: DateField::new(default_date), - end: DateField::new(default_date), + start, + end, on_search: RefCell::new(Box::new(|_| {})), } } @@ -56,7 +58,6 @@ glib::wrapper! { pub struct DateRangePicker(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; } - impl DateRangePicker { pub fn connect_on_search(&self, f: OnSearch) where @@ -78,19 +79,25 @@ impl DateRangePicker { } } - impl Default for DateRangePicker { fn default() -> Self { let s: Self = Object::builder().build(); s.set_orientation(gtk::Orientation::Vertical); + s.add_css_class("date-range-picker"); - let search_button = gtk::Button::with_label("Search"); + let search_button = gtk::Button::builder() + .css_classes(["date-range-picker__search-button"]) + .label("Search") + .build(); search_button.connect_clicked({ let s = s.clone(); move |_| (s.imp().on_search.borrow())(s.interval()) }); - let last_week_button = gtk::Button::builder().label("last week").build(); + let last_week_button = gtk::Button::builder() + .css_classes(["date-range-picker__range-button"]) + .label("week") + .build(); last_week_button.connect_clicked({ let s = s.clone(); move |_| { @@ -101,7 +108,10 @@ impl Default for DateRangePicker { } }); - let two_weeks_button = gtk::Button::builder().label("last two weeks").build(); + let two_weeks_button = gtk::Button::builder() + .css_classes(["date-range-picker__range-button"]) + .label("two weeks") + .build(); two_weeks_button.connect_clicked({ let s = s.clone(); move |_| { @@ -112,7 +122,10 @@ impl Default for DateRangePicker { } }); - let last_month_button = gtk::Button::builder().label("last month").build(); + let last_month_button = gtk::Button::builder() + .css_classes(["date-range-picker__range-button"]) + .label("month") + .build(); last_month_button.connect_clicked({ let s = s.clone(); move |_| { @@ -123,7 +136,10 @@ impl Default for DateRangePicker { } }); - let six_months_button = gtk::Button::builder().label("last six months").build(); + let six_months_button = gtk::Button::builder() + .css_classes(["date-range-picker__range-button"]) + .label("six months") + .build(); six_months_button.connect_clicked({ let s = s.clone(); move |_| { @@ -134,7 +150,10 @@ impl Default for DateRangePicker { } }); - let last_year_button = gtk::Button::builder().label("last year").build(); + let last_year_button = gtk::Button::builder() + .css_classes(["date-range-picker__range-button"]) + .label("year") + .build(); last_year_button.connect_clicked({ let s = s.clone(); move |_| { diff --git a/fitnesstrax/app/src/views/historical_view.rs b/fitnesstrax/app/src/views/historical_view.rs index ec7b384..da025b4 100644 --- a/fitnesstrax/app/src/views/historical_view.rs +++ b/fitnesstrax/app/src/views/historical_view.rs @@ -141,7 +141,10 @@ impl HistoricalView { pub fn set_interval(&self, interval: DayInterval) { let mut model = gio::ListStore::new::(); - model.extend(interval.days().map(Date::new)); + let mut days = interval.days().map(Date::new).collect::>(); + days.reverse(); + model.extend(days.into_iter()); + // model.extend(interval.days().map(Date::new)); self.imp() .list_view .set_model(Some(>k::NoSelection::new(Some(model)))); -- 2.44.1 From a5d51dab707246261cab0b00f5b0c8a212b233f7 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 19 Feb 2024 17:56:48 -0500 Subject: [PATCH 3/5] Hugely improve the historical view formatting --- fitnesstrax/app/resources/style.css | 16 ++++++---------- fitnesstrax/app/src/components/day.rs | 7 +++---- fitnesstrax/app/src/components/time_distance.rs | 2 +- fitnesstrax/app/src/types.rs | 12 ++++++------ fitnesstrax/app/src/views/historical_view.rs | 1 + 5 files changed, 17 insertions(+), 21 deletions(-) diff --git a/fitnesstrax/app/resources/style.css b/fitnesstrax/app/resources/style.css index 696a917..b7c7dec 100644 --- a/fitnesstrax/app/resources/style.css +++ b/fitnesstrax/app/resources/style.css @@ -3,7 +3,7 @@ } .welcome__title { - font-size: larger; + font-size: x-large; padding: 8px; } @@ -28,21 +28,14 @@ .date-range-picker__date-field { margin: 8px; - font-size: x-large; } .date-range-picker__search-button { margin: 8px; - font-size: x-large; } .date-range-picker__range-button { margin: 8px; - font-size: x-large; -} - -.date-field > label { - font-size: x-large; } .date-field__year { @@ -61,11 +54,14 @@ padding: 8px; } -.day-summary__date { - font-size: larger; +.day-summary > *:not(:last-child) { margin-bottom: 8px; } +.day-summary__date { + font-size: x-large; +} + .day-summary__weight { margin: 4px; } diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index 0402829..f2fa124 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -91,17 +91,16 @@ impl DaySummary { if let Some(w) = view_model.weight() { label.set_label(&w.to_string()) } - row.append(&label); - - self.append(&label); let label = gtk::Label::builder() .halign(gtk::Align::Start) - .css_classes(["day-summary__weight"]) + .css_classes(["day-summary__steps"]) .build(); if let Some(s) = view_model.steps() { label.set_label(&format!("{} steps", s)); } + + row.append(&label); row.append(&label); self.append(&row); diff --git a/fitnesstrax/app/src/components/time_distance.rs b/fitnesstrax/app/src/components/time_distance.rs index 6ad5c3a..b06ca8f 100644 --- a/fitnesstrax/app/src/components/time_distance.rs +++ b/fitnesstrax/app/src/components/time_distance.rs @@ -49,7 +49,7 @@ pub fn time_distance_summary( (false, false) => None, }; - text.map(|text| gtk::Label::new(Some(&text))) + text.map(|text| gtk::Label::builder().halign(gtk::Align::Start).label(&text).build()) } pub fn time_distance_detail(record: ft_core::TimeDistance) -> gtk::Box { diff --git a/fitnesstrax/app/src/types.rs b/fitnesstrax/app/src/types.rs index 8ed75fd..36a7252 100644 --- a/fitnesstrax/app/src/types.rs +++ b/fitnesstrax/app/src/types.rs @@ -79,12 +79,12 @@ impl TimeFormatter { match parts.len() { 0 => Err(ParseError), 1 => Err(ParseError), - 2 => Ok(TimeFormatter( - chrono::NaiveTime::from_hms_opt(parts[0], parts[1], 0).unwrap(), - )), - 3 => Ok(TimeFormatter( - chrono::NaiveTime::from_hms_opt(parts[0], parts[1], parts[2]).unwrap(), - )), + 2 => chrono::NaiveTime::from_hms_opt(parts[0], parts[1], 0) + .map(|v| TimeFormatter(v)) + .ok_or(ParseError), + 3 => chrono::NaiveTime::from_hms_opt(parts[0], parts[1], parts[2]) + .map(|v| TimeFormatter(v)) + .ok_or(ParseError), _ => Err(ParseError), } } diff --git a/fitnesstrax/app/src/views/historical_view.rs b/fitnesstrax/app/src/views/historical_view.rs index da025b4..2d04d74 100644 --- a/fitnesstrax/app/src/views/historical_view.rs +++ b/fitnesstrax/app/src/views/historical_view.rs @@ -55,6 +55,7 @@ impl ObjectSubclass for HistoricalViewPrivate { list_view: gtk::ListView::builder() .factory(&factory) .single_click_activate(true) + .show_separators(true) .build(), date_range_picker, }; -- 2.44.1 From 0032f164223e0ed51d68112bdc1ae404ebc9a90d Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 19 Feb 2024 18:04:45 -0500 Subject: [PATCH 4/5] Resolve the duplicate widget insertion warning --- fitnesstrax/app/src/components/day.rs | 12 ++++++------ fitnesstrax/app/src/views/historical_view.rs | 1 - 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index f2fa124..5479446 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -84,24 +84,24 @@ impl DaySummary { let row = gtk::Box::builder().build(); - let label = gtk::Label::builder() + let weight_label = gtk::Label::builder() .halign(gtk::Align::Start) .css_classes(["day-summary__weight"]) .build(); if let Some(w) = view_model.weight() { - label.set_label(&w.to_string()) + weight_label.set_label(&w.to_string()) } - let label = gtk::Label::builder() + let steps_label = gtk::Label::builder() .halign(gtk::Align::Start) .css_classes(["day-summary__steps"]) .build(); if let Some(s) = view_model.steps() { - label.set_label(&format!("{} steps", s)); + steps_label.set_label(&format!("{} steps", s)); } - row.append(&label); - row.append(&label); + row.append(&weight_label); + row.append(&steps_label); self.append(&row); for activity in TIME_DISTANCE_ACTIVITIES { diff --git a/fitnesstrax/app/src/views/historical_view.rs b/fitnesstrax/app/src/views/historical_view.rs index 2d04d74..8357c74 100644 --- a/fitnesstrax/app/src/views/historical_view.rs +++ b/fitnesstrax/app/src/views/historical_view.rs @@ -145,7 +145,6 @@ impl HistoricalView { let mut days = interval.days().map(Date::new).collect::>(); days.reverse(); model.extend(days.into_iter()); - // model.extend(interval.days().map(Date::new)); self.imp() .list_view .set_model(Some(>k::NoSelection::new(Some(model)))); -- 2.44.1 From 9e7350b08713a66c2e5e4cb6214aae742b65e7e8 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 19 Feb 2024 18:41:38 -0500 Subject: [PATCH 5/5] Add an about page that calls out gtk-rs and the GUI development book --- fitnesstrax/app/resources/style.css | 8 +++ fitnesstrax/app/src/about.rs | 80 +++++++++++++++++++++++++++++ fitnesstrax/app/src/app_window.rs | 1 + fitnesstrax/app/src/main.rs | 13 ++++- 4 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 fitnesstrax/app/src/about.rs diff --git a/fitnesstrax/app/resources/style.css b/fitnesstrax/app/resources/style.css index b7c7dec..55916f5 100644 --- a/fitnesstrax/app/resources/style.css +++ b/fitnesstrax/app/resources/style.css @@ -74,4 +74,12 @@ .step-view { padding: 8px; margin: 8px; +} + +.about__content { + padding: 32px; +} + +.about label { + margin-bottom: 16px; } \ No newline at end of file diff --git a/fitnesstrax/app/src/about.rs b/fitnesstrax/app/src/about.rs new file mode 100644 index 0000000..73f85c3 --- /dev/null +++ b/fitnesstrax/app/src/about.rs @@ -0,0 +1,80 @@ +/* +Copyright 2023 - 2024, Savanni D'Gerinel + +This file is part of FitnessTrax. + +FitnessTrax is free software: you can redistribute it and/or modify it under the terms of the GNU +General Public License as published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +FitnessTrax is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without +even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see . +*/ + +use glib::Object; +use gtk::{prelude::*, subclass::prelude::*}; + +#[derive(Default)] +pub struct AboutWindowPrivate {} + +#[glib::object_subclass] +impl ObjectSubclass for AboutWindowPrivate { + const NAME: &'static str = "AboutWindow"; + type Type = AboutWindow; + type ParentType = gtk::Window; +} + +impl ObjectImpl for AboutWindowPrivate {} +impl WidgetImpl for AboutWindowPrivate {} +impl WindowImpl for AboutWindowPrivate {} + +glib::wrapper! { + pub struct AboutWindow(ObjectSubclass) @extends gtk::Window, gtk::Widget; +} + +impl Default for AboutWindow { + fn default() -> Self { + let s: Self = Object::builder().build(); + s.set_width_request(600); + s.set_height_request(700); + s.add_css_class("about"); + + s.set_title(Some("About Fitnesstrax")); + let copyright = gtk::Label::builder() + .label("Copyright 2023-2024, Savanni D'Gerinel ") + .halign(gtk::Align::Start) + .build(); + + let gtk_rs_thanks = gtk::Label::builder() + .label("I owe a huge debt of gratitude to the GTK-RS project (https://gtk-rs.org/), which makes it possible for me to write this application to begin with. Further, I owe a particular debt to Julian Hofer and his book, GUI development with Rust and GTK 4 (https://gtk-rs.org/gtk4-rs/stable/latest/book/). Without this book, I would have continued to stumble around writing bad user interfaces with even worse code.") + .halign(gtk::Align::Start).wrap(true) + .build(); + + let dependencies = gtk::Label::builder() + .label("This application depends on many libraries, most of which are licensed under the BSD-3 or GPL-3 licenses.") + .halign(gtk::Align::Start).wrap(true) + .build(); + + let content = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .css_classes(["about__content"]) + .build(); + content.append(©right); + content.append(>k_rs_thanks); + content.append(&dependencies); + + let scroller = gtk::ScrolledWindow::builder() + .child(&content) + .hexpand(true) + .vexpand(true) + .hscrollbar_policy(gtk::PolicyType::Never) + .build(); + + s.set_child(Some(&scroller)); + + s + } +} diff --git a/fitnesstrax/app/src/app_window.rs b/fitnesstrax/app/src/app_window.rs index 3bdd4a4..81a9eb9 100644 --- a/fitnesstrax/app/src/app_window.rs +++ b/fitnesstrax/app/src/app_window.rs @@ -89,6 +89,7 @@ impl AppWindow { let header_bar = adw::HeaderBar::new(); let main_menu = gio::Menu::new(); + main_menu.append(Some("About"), Some("app.about")); main_menu.append(Some("Quit"), Some("app.quit")); let main_menu_button = gtk::MenuButton::builder() .icon_name("open-menu") diff --git a/fitnesstrax/app/src/main.rs b/fitnesstrax/app/src/main.rs index b6ca965..98a8cc2 100644 --- a/fitnesstrax/app/src/main.rs +++ b/fitnesstrax/app/src/main.rs @@ -1,5 +1,5 @@ /* -Copyright 2023, Savanni D'Gerinel +Copyright 2023 - 2024, Savanni D'Gerinel This file is part of FitnessTrax. @@ -14,6 +14,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see . */ +mod about; mod app; mod app_window; mod components; @@ -33,6 +34,15 @@ const APP_ID_PROD: &str = "com.luminescent-dreams.fitnesstrax"; const RESOURCE_BASE_PATH: &str = "/com/luminescent-dreams/fitnesstrax/"; +fn setup_app_about_action(app: &adw::Application) { + let action = ActionEntry::builder("about") + .activate(|app: &adw::Application, _, _| { + let window = about::AboutWindow::default(); + window.present(); + }).build(); + app.add_action_entries([action]); +} + /// Sets up an application-global action, `app.quit`, which will terminate the application. fn setup_app_close_action(app: &adw::Application) { let action = ActionEntry::builder("quit") @@ -80,6 +90,7 @@ fn main() { let icon_theme = gtk::IconTheme::for_display(&gdk::Display::default().unwrap()); icon_theme.add_resource_path(&(RESOURCE_BASE_PATH.to_owned() + "/icons/scalable/actions")); + setup_app_about_action(adw_app); setup_app_close_action(adw_app); AppWindow::new(app_id, RESOURCE_BASE_PATH, adw_app, ft_app.clone()); -- 2.44.1