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)