Filter the historical list according to a date picker #194

Merged
savanni merged 7 commits from fitnesstrax/date-field into main 2024-02-18 22:19:40 +00:00
4 changed files with 155 additions and 27 deletions
Showing only changes of commit cd6c4d5c76 - Show all commits

View File

@ -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 <https://www.gnu.org/licenses/>. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
*/ */
use crate::{components::TextEntry, types::ParseError}; use crate::{components::{i32_field, TextEntry, month_field}, types::ParseError};
use chrono::{Datelike, Local}; use chrono::{Datelike, Local};
use glib::Object; use glib::Object;
use gtk::{prelude::*, subclass::prelude::*}; use gtk::{prelude::*, subclass::prelude::*};
@ -28,7 +28,7 @@ enum Part {
} }
pub struct DateFieldPrivate { pub struct DateFieldPrivate {
value: Rc<RefCell<chrono::NaiveDate>>, date: Rc<RefCell<chrono::NaiveDate>>,
year: TextEntry<i32>, year: TextEntry<i32>,
month: TextEntry<u32>, month: TextEntry<u32>,
day: TextEntry<u32>, day: TextEntry<u32>,
@ -41,36 +41,69 @@ impl ObjectSubclass for DateFieldPrivate {
type ParentType = gtk::Box; type ParentType = gtk::Box;
fn new() -> Self { fn new() -> Self {
let date = Local::now().date_naive(); let date = Rc::new(RefCell::new(Local::now().date_naive()));
let year = TextEntry::new(
"year", let year = i32_field(date.borrow().year(),
Some(date.year()), {
|v| format!("{}", v), let date = date.clone();
|v| v.parse::<i32>().map_err(|_| ParseError), 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( year.widget.set_max_length(4);
"month", year.add_css_class("date-field__year");
Some(date.month()),
|v| format!("{}", v), let month = month_field(
|v| v.parse::<u32>().map_err(|_| ParseError), 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( let day = TextEntry::new(
"day", "day",
Some(date.day()), Some(date.borrow().day()),
|v| format!("{}", v), |v| format!("{}", v),
|v| v.parse::<u32>().map_err(|_| ParseError), |v| v.parse::<u32>().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 { Self {
value: Rc::new(RefCell::new(date.clone())), date,
year, year,
month, month,
day, day,
} }
} }
} }
impl ObjectImpl for DateFieldPrivate {} impl ObjectImpl for DateFieldPrivate {}
@ -97,17 +130,48 @@ impl DateField {
s.imp().month.set_value(Some(date.month())); s.imp().month.set_value(Some(date.month()));
s.imp().day.set_value(Some(date.day())); s.imp().day.set_value(Some(date.day()));
*s.imp().value.borrow_mut() = date; *s.imp().date.borrow_mut() = date;
s 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)] #[cfg(test)]
mod test { mod test {
use super::*; 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]
#[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!()
}
} }

View File

@ -31,7 +31,7 @@ mod steps;
pub use steps::{steps_editor, Steps}; pub use steps::{steps_editor, Steps};
mod text_entry; 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; mod time_distance;
pub use time_distance::{time_distance_detail, time_distance_summary}; pub use time_distance::{time_distance_detail, time_distance_summary};

View File

@ -23,11 +23,12 @@ use std::{cell::RefCell, rc::Rc};
pub type Parser<T> = dyn Fn(&str) -> Result<T, ParseError>; pub type Parser<T> = dyn Fn(&str) -> Result<T, ParseError>;
pub type OnUpdate<T> = dyn Fn(Option<T>); pub type OnUpdate<T> = dyn Fn(Option<T>);
// 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)] #[derive(Clone)]
pub struct TextEntry<T: Clone + std::fmt::Debug> { pub struct TextEntry<T: Clone + std::fmt::Debug> {
value: Rc<RefCell<Option<T>>>, value: Rc<RefCell<Option<T>>>,
widget: gtk::Entry, pub widget: gtk::Entry,
renderer: Rc<dyn Fn(&T) -> String>, renderer: Rc<dyn Fn(&T) -> String>,
parser: Rc<Parser<T>>, parser: Rc<Parser<T>>,
on_update: Rc<OnUpdate<T>>, on_update: Rc<OnUpdate<T>>,
@ -82,7 +83,6 @@ impl<T: Clone + std::fmt::Debug + 'static> TextEntry<T> {
if let Some(ref v) = val { if let Some(ref v) = val {
self.widget.set_text(&(self.renderer)(v)); self.widget.set_text(&(self.renderer)(v));
} }
*self.value.borrow_mut() = val;
} }
fn handle_text_change(&self, buffer: &gtk::EntryBuffer) { fn handle_text_change(&self, buffer: &gtk::EntryBuffer) {
@ -105,6 +105,14 @@ impl<T: Clone + std::fmt::Debug + 'static> TextEntry<T> {
} }
} }
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 { pub fn widget(&self) -> gtk::Widget {
self.widget.clone().upcast::<gtk::Widget>() self.widget.clone().upcast::<gtk::Widget>()
} }
@ -181,6 +189,45 @@ where
) )
} }
pub fn i32_field<OnUpdate>(
value: i32,
on_update: OnUpdate,
) -> TextEntry<i32>
where
OnUpdate: Fn(Option<i32>) + 'static,
{
TextEntry::new(
"0",
Some(value),
|val| format!("{}", val),
|v|
v.parse::<i32>().map_err(|_| ParseError),
on_update,
)
}
pub fn month_field<OnUpdate>(
value: u32,
on_update: OnUpdate,
) -> TextEntry<u32>
where
OnUpdate: Fn(Option<u32>) + 'static,
{
TextEntry::new(
"0",
Some(value),
|val| format!("{}", val),
|v| {
let val = v.parse::<u32>().map_err(|_| ParseError)?;
if val == 0 || val > 12 {
return Err(ParseError);
}
Ok(val)
},
on_update,
)
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;

View File

@ -109,12 +109,29 @@ impl HistoricalView {
*s.imp().app.borrow_mut() = Some(app); *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() let date_row = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal) .orientation(gtk::Orientation::Horizontal)
.build(); .build();
date_row.append(&DateField::new(interval.start)); date_row.append(&start_date);
date_row.append(&gtk::Label::new(Some("to"))); date_row.append(&gtk::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() let quick_picker = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal) .orientation(gtk::Orientation::Horizontal)