From cd6c4d5c76212f6b1832176c01e1277750c7e663 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Fri, 16 Feb 2024 09:16:42 -0500 Subject: [PATCH] Apply the value of a date field to the historical view Date fields can now be updated and their values retrieved. So now I have added a button that retrieves the range of the date field and updates the historical view in response to them. --- fitnesstrax/app/src/components/date_field.rs | 108 +++++++++++++++---- fitnesstrax/app/src/components/mod.rs | 2 +- fitnesstrax/app/src/components/text_entry.rs | 51 ++++++++- fitnesstrax/app/src/views/historical_view.rs | 21 +++- 4 files changed, 155 insertions(+), 27 deletions(-) diff --git a/fitnesstrax/app/src/components/date_field.rs b/fitnesstrax/app/src/components/date_field.rs index 64bf7ab..7b56330 100644 --- a/fitnesstrax/app/src/components/date_field.rs +++ b/fitnesstrax/app/src/components/date_field.rs @@ -14,7 +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 . */ -use crate::{components::TextEntry, types::ParseError}; +use crate::{components::{i32_field, TextEntry, month_field}, types::ParseError}; use chrono::{Datelike, Local}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; @@ -28,7 +28,7 @@ enum Part { } pub struct DateFieldPrivate { - value: Rc>, + date: Rc>, year: TextEntry, month: TextEntry, day: TextEntry, @@ -41,36 +41,69 @@ impl ObjectSubclass for DateFieldPrivate { type ParentType = gtk::Box; fn new() -> Self { - let date = Local::now().date_naive(); - let year = TextEntry::new( - "year", - Some(date.year()), - |v| format!("{}", v), - |v| v.parse::().map_err(|_| ParseError), - |_| {}, + let date = Rc::new(RefCell::new(Local::now().date_naive())); + + let year = i32_field(date.borrow().year(), + { + let date = date.clone(); + move |value| { + if let Some(year) = value { + let mut date = date.borrow_mut(); + if let Some(new_date) = date.with_year(year) { + *date = new_date; + } + } + } + }, ); - let month = TextEntry::new( - "month", - Some(date.month()), - |v| format!("{}", v), - |v| v.parse::().map_err(|_| ParseError), - |_| {}, + year.widget.set_max_length(4); + year.add_css_class("date-field__year"); + + let month = month_field( + date.borrow().month(), + { + let date = date.clone(); + move |value| { + if let Some(month) = value { + let mut date = date.borrow_mut(); + if let Some(new_date) = date.with_month(month) { + *date = new_date; + } + } + } + }, ); + month.add_css_class("date-field__month"); + + /* Modify this so that it enforces the number of days per month */ let day = TextEntry::new( "day", - Some(date.day()), + Some(date.borrow().day()), |v| format!("{}", v), |v| v.parse::().map_err(|_| ParseError), - |_| {}, + { + let date = date.clone(); + move |value| { + if let Some(day) = value { + let mut date = date.borrow_mut(); + if let Some(new_date) = date.with_day(day) { + *date = new_date; + } + } + } + }, + ); + day.add_css_class("date-field__day"); Self { - value: Rc::new(RefCell::new(date.clone())), + date, year, month, day, } } + } impl ObjectImpl for DateFieldPrivate {} @@ -97,17 +130,48 @@ impl DateField { s.imp().month.set_value(Some(date.month())); s.imp().day.set_value(Some(date.day())); - *s.imp().value.borrow_mut() = date; + *s.imp().date.borrow_mut() = date; s } -} -/* As soon as the field gets focus, highlight the first element - */ + + pub fn date(&self) -> chrono::NaiveDate { + self.imp().date.borrow().clone() + } + /* + pub fn is_valid(&self) -> bool { + false + } + */ +} #[cfg(test)] mod test { use super::*; 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] + #[ignore] + fn it_allows_valid_dates() { + let reference = chrono::NaiveDate::from_ymd_opt(2006, 01, 02).unwrap(); + let field = DateField::new(reference); + field.imp().year.set_value(Some(2023)); + field.imp().month.set_value(Some(10)); + field.imp().day.set_value(Some(13)); + assert!(field.is_valid()); + } + + #[test] + #[ignore] + fn it_disallows_out_of_range_months() { + unimplemented!() + } + + #[test] + #[ignore] + fn it_allows_days_within_range_for_month() { + unimplemented!() + } } diff --git a/fitnesstrax/app/src/components/mod.rs b/fitnesstrax/app/src/components/mod.rs index d2f9d26..addb82f 100644 --- a/fitnesstrax/app/src/components/mod.rs +++ b/fitnesstrax/app/src/components/mod.rs @@ -31,7 +31,7 @@ mod steps; pub use steps::{steps_editor, Steps}; mod text_entry; -pub use text_entry::{distance_field, duration_field, time_field, weight_field, TextEntry}; +pub use text_entry::{distance_field, duration_field, time_field, weight_field, i32_field, month_field, TextEntry}; mod time_distance; pub use time_distance::{time_distance_detail, time_distance_summary}; diff --git a/fitnesstrax/app/src/components/text_entry.rs b/fitnesstrax/app/src/components/text_entry.rs index 71a4ed4..74615ab 100644 --- a/fitnesstrax/app/src/components/text_entry.rs +++ b/fitnesstrax/app/src/components/text_entry.rs @@ -23,11 +23,12 @@ use std::{cell::RefCell, rc::Rc}; pub type Parser = dyn Fn(&str) -> Result; pub type OnUpdate = dyn Fn(Option); +// TextEntry is not a proper widget because I was never able to figure out how to do a type parameterization on a GTK widget. #[derive(Clone)] pub struct TextEntry { value: Rc>>, - widget: gtk::Entry, + pub widget: gtk::Entry, renderer: Rc String>, parser: Rc>, on_update: Rc>, @@ -82,7 +83,6 @@ impl TextEntry { if let Some(ref v) = val { self.widget.set_text(&(self.renderer)(v)); } - *self.value.borrow_mut() = val; } fn handle_text_change(&self, buffer: >k::EntryBuffer) { @@ -105,6 +105,14 @@ impl TextEntry { } } + pub fn add_css_class(&self, class_: &str) { + self.widget.add_css_class(class_); + } + + pub fn remove_css_class(&self, class_: &str) { + self.widget.remove_css_class(class_); + } + pub fn widget(&self) -> gtk::Widget { self.widget.clone().upcast::() } @@ -181,6 +189,45 @@ where ) } +pub fn i32_field( + value: i32, + on_update: OnUpdate, +) -> TextEntry +where + OnUpdate: Fn(Option) + 'static, +{ + TextEntry::new( + "0", + Some(value), + |val| format!("{}", val), + |v| + v.parse::().map_err(|_| ParseError), + on_update, + ) +} + +pub fn month_field( + value: u32, + on_update: OnUpdate, +) -> TextEntry +where + OnUpdate: Fn(Option) + 'static, +{ + TextEntry::new( + "0", + Some(value), + |val| format!("{}", val), + |v| { + let val = v.parse::().map_err(|_| ParseError)?; + if val == 0 || val > 12 { + return Err(ParseError); + } + Ok(val) + }, + on_update, + ) +} + #[cfg(test)] mod test { use super::*; diff --git a/fitnesstrax/app/src/views/historical_view.rs b/fitnesstrax/app/src/views/historical_view.rs index 51ec3a5..7bda8ea 100644 --- a/fitnesstrax/app/src/views/historical_view.rs +++ b/fitnesstrax/app/src/views/historical_view.rs @@ -109,12 +109,29 @@ impl HistoricalView { *s.imp().app.borrow_mut() = Some(app); + let start_date = DateField::new(interval.start); + let end_date = DateField::new(interval.end); + let search_button = gtk::Button::with_label("Search"); + search_button.connect_clicked({ + let s = s.clone(); + let start_date = start_date.clone(); + let end_date = end_date.clone(); + move |_| { + let interval = DayInterval { + start: start_date.date(), + end: end_date.date(), + }; + s.set_interval(interval); + } + }); + let date_row = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .build(); - date_row.append(&DateField::new(interval.start)); + date_row.append(&start_date); date_row.append(>k::Label::new(Some("to"))); - date_row.append(&DateField::new(interval.end)); + date_row.append(&end_date); + date_row.append(&search_button); let quick_picker = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal)