From e87b332705401e52c3c697eb3921f21a9566eff4 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sat, 10 Feb 2024 11:38:25 -0500 Subject: [PATCH 1/7] Start building an internally editable date field --- fitnesstrax/app/src/components/date_field.rs | 75 ++++++++++++++++++++ fitnesstrax/app/src/components/mod.rs | 3 + fitnesstrax/app/src/views/historical_view.rs | 23 +++++- 3 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 fitnesstrax/app/src/components/date_field.rs diff --git a/fitnesstrax/app/src/components/date_field.rs b/fitnesstrax/app/src/components/date_field.rs new file mode 100644 index 0000000..6cc5335 --- /dev/null +++ b/fitnesstrax/app/src/components/date_field.rs @@ -0,0 +1,75 @@ +/* +Copyright 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 crate::types::ParseError; +use chrono::Datelike; +use gtk::prelude::*; +use std::{cell::RefCell, rc::Rc}; + +#[derive(Clone)] +pub struct DateField { + value: Rc>, + buffer: gtk::TextBuffer, + widget: gtk::TextView, +} + +impl DateField { + pub fn new(date: chrono::NaiveDate) -> Self { + let buffer = gtk::TextBuffer::new(None); + let tag = buffer.create_tag( + Some("placeholder"), + &[("foreground", &String::from("grey"))], + ); + + buffer.set_text("regular text placeholder text"); + let (start, end) = buffer.bounds(); + let mut placeholder_start = start.clone(); + placeholder_start.forward_chars(13); + + let placeholder_markup = buffer.tag_table().lookup("placeholder").unwrap(); + buffer.apply_tag(&placeholder_markup, &placeholder_start, &end); + + let widget = gtk::TextView::builder() + .buffer(&buffer) + .editable(true) + .can_focus(true) + .height_request(50) + .width_request(200) + .build(); + + let s = Self { + value: Rc::new(RefCell::new(date)), + buffer, + widget, + }; + + s.widget.buffer().connect_text_notify({ + let s = s.clone(); + move |buffer| s.on_update(buffer) + }); + + s + } + + pub fn widget(&self) -> gtk::Widget { + self.widget.clone().upcast::() + } + + fn on_update(&self, buffer: >k::TextBuffer) { + let (start, end) = buffer.bounds(); + println!("[on_update] {}", buffer.text(&start, &end, true)); + } +} diff --git a/fitnesstrax/app/src/components/mod.rs b/fitnesstrax/app/src/components/mod.rs index c1da4fd..d2f9d26 100644 --- a/fitnesstrax/app/src/components/mod.rs +++ b/fitnesstrax/app/src/components/mod.rs @@ -21,6 +21,9 @@ pub use action_group::ActionGroup; mod day; pub use day::{DayDetail, DayEdit, DaySummary}; +mod date_field; +pub use date_field::DateField; + mod singleton; pub use singleton::{Singleton, SingletonImpl}; diff --git a/fitnesstrax/app/src/views/historical_view.rs b/fitnesstrax/app/src/views/historical_view.rs index ead6184..a7accc4 100644 --- a/fitnesstrax/app/src/views/historical_view.rs +++ b/fitnesstrax/app/src/views/historical_view.rs @@ -15,7 +15,10 @@ You should have received a copy of the GNU General Public License along with Fit */ use crate::{ - app::App, components::DaySummary, types::DayInterval, view_models::DayDetailViewModel, + app::App, + components::{DateField, DaySummary}, + types::DayInterval, + view_models::DayDetailViewModel, }; use glib::Object; @@ -106,6 +109,22 @@ impl HistoricalView { *s.imp().app.borrow_mut() = Some(app); + let date_row = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .build(); + date_row.append(&DateField::new(interval.start).widget()); + date_row.append(>k::Label::new(Some("to"))); + date_row.append(&DateField::new(interval.end).widget()); + + let quick_picker = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .build(); + quick_picker.append(>k::Button::builder().label("last week").build()); + quick_picker.append(>k::Button::builder().label("last two weeks").build()); + quick_picker.append(>k::Button::builder().label("last month").build()); + quick_picker.append(>k::Button::builder().label("last six months").build()); + quick_picker.append(>k::Button::builder().label("last year").build()); + let mut model = gio::ListStore::new::(); model.extend(interval.days().map(Date::new)); s.imp() @@ -122,6 +141,8 @@ impl HistoricalView { } }); + s.append(&date_row); + s.append(&quick_picker); s.append(&s.imp().list_view); s -- 2.44.1 From 3d6e5470ed369dd4ed44600d684f4ab153efedbf Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Thu, 15 Feb 2024 09:11:34 -0500 Subject: [PATCH 2/7] Start setting up date range widgets --- fitnesstrax/app/src/components/date_field.rs | 128 ++++++++++++------- fitnesstrax/app/src/components/text_entry.rs | 9 ++ fitnesstrax/app/src/views/historical_view.rs | 4 +- 3 files changed, 94 insertions(+), 47 deletions(-) diff --git a/fitnesstrax/app/src/components/date_field.rs b/fitnesstrax/app/src/components/date_field.rs index 6cc5335..64bf7ab 100644 --- a/fitnesstrax/app/src/components/date_field.rs +++ b/fitnesstrax/app/src/components/date_field.rs @@ -14,62 +14,100 @@ 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::types::ParseError; -use chrono::Datelike; -use gtk::prelude::*; +use crate::{components::TextEntry, types::ParseError}; +use chrono::{Datelike, Local}; +use glib::Object; +use gtk::{prelude::*, subclass::prelude::*}; use std::{cell::RefCell, rc::Rc}; -#[derive(Clone)] -pub struct DateField { - value: Rc>, - buffer: gtk::TextBuffer, - widget: gtk::TextView, +#[derive(Clone, Debug)] +enum Part { + Year, + Month, + Day, } -impl DateField { - pub fn new(date: chrono::NaiveDate) -> Self { - let buffer = gtk::TextBuffer::new(None); - let tag = buffer.create_tag( - Some("placeholder"), - &[("foreground", &String::from("grey"))], +pub struct DateFieldPrivate { + value: Rc>, + year: TextEntry, + month: TextEntry, + day: TextEntry, +} + +#[glib::object_subclass] +impl ObjectSubclass for DateFieldPrivate { + const NAME: &'static str = "DateField"; + type Type = DateField; + 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 month = TextEntry::new( + "month", + Some(date.month()), + |v| format!("{}", v), + |v| v.parse::().map_err(|_| ParseError), + |_| {}, + ); + let day = TextEntry::new( + "day", + Some(date.day()), + |v| format!("{}", v), + |v| v.parse::().map_err(|_| ParseError), + |_| {}, ); - buffer.set_text("regular text placeholder text"); - let (start, end) = buffer.bounds(); - let mut placeholder_start = start.clone(); - placeholder_start.forward_chars(13); + Self { + value: Rc::new(RefCell::new(date.clone())), + year, + month, + day, + } + } +} - let placeholder_markup = buffer.tag_table().lookup("placeholder").unwrap(); - buffer.apply_tag(&placeholder_markup, &placeholder_start, &end); +impl ObjectImpl for DateFieldPrivate {} +impl WidgetImpl for DateFieldPrivate {} +impl BoxImpl for DateFieldPrivate {} - let widget = gtk::TextView::builder() - .buffer(&buffer) - .editable(true) - .can_focus(true) - .height_request(50) - .width_request(200) - .build(); +glib::wrapper! { + pub struct DateField(ObjectSubclass) @extends gtk::Box, gtk::Widget; +} - let s = Self { - value: Rc::new(RefCell::new(date)), - buffer, - widget, - }; +/* Render a date in the format 2006 Jan 01. The entire date is editable. When the user moves to one part of the date, it will be erased and replaced with a grey placeholder. + */ +impl DateField { + pub fn new(date: chrono::NaiveDate) -> Self { + let s: Self = Object::builder().build(); - s.widget.buffer().connect_text_notify({ - let s = s.clone(); - move |buffer| s.on_update(buffer) - }); + println!("{}", date); + + s.append(&s.imp().year.widget()); + s.append(&s.imp().month.widget()); + s.append(&s.imp().day.widget()); + + s.imp().year.set_value(Some(date.year())); + s.imp().month.set_value(Some(date.month())); + s.imp().day.set_value(Some(date.day())); + + *s.imp().value.borrow_mut() = date; s } - - pub fn widget(&self) -> gtk::Widget { - self.widget.clone().upcast::() - } - - fn on_update(&self, buffer: >k::TextBuffer) { - let (start, end) = buffer.bounds(); - println!("[on_update] {}", buffer.text(&start, &end, true)); - } +} + +/* As soon as the field gets focus, highlight the first element + */ + +#[cfg(test)] +mod test { + use super::*; + use crate::gtk_init::gtk_init; } diff --git a/fitnesstrax/app/src/components/text_entry.rs b/fitnesstrax/app/src/components/text_entry.rs index a8f89cb..71a4ed4 100644 --- a/fitnesstrax/app/src/components/text_entry.rs +++ b/fitnesstrax/app/src/components/text_entry.rs @@ -28,6 +28,7 @@ pub struct TextEntry { value: Rc>>, widget: gtk::Entry, + renderer: Rc String>, parser: Rc>, on_update: Rc>, } @@ -64,6 +65,7 @@ impl TextEntry { let s = Self { value: Rc::new(RefCell::new(value)), widget, + renderer: Rc::new(renderer), parser: Rc::new(parser), on_update: Rc::new(on_update), }; @@ -76,6 +78,13 @@ impl TextEntry { s } + pub fn set_value(&self, val: Option) { + 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) { if buffer.text().is_empty() { *self.value.borrow_mut() = None; diff --git a/fitnesstrax/app/src/views/historical_view.rs b/fitnesstrax/app/src/views/historical_view.rs index a7accc4..51ec3a5 100644 --- a/fitnesstrax/app/src/views/historical_view.rs +++ b/fitnesstrax/app/src/views/historical_view.rs @@ -112,9 +112,9 @@ impl HistoricalView { let date_row = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .build(); - date_row.append(&DateField::new(interval.start).widget()); + date_row.append(&DateField::new(interval.start)); date_row.append(>k::Label::new(Some("to"))); - date_row.append(&DateField::new(interval.end).widget()); + date_row.append(&DateField::new(interval.end)); let quick_picker = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) -- 2.44.1 From cd6c4d5c76212f6b1832176c01e1277750c7e663 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Fri, 16 Feb 2024 09:16:42 -0500 Subject: [PATCH 3/7] 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) -- 2.44.1 From 965d03e208a42fefd5eb9d3cc8d9bda1880cd679 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Fri, 16 Feb 2024 18:21:34 -0500 Subject: [PATCH 4/7] Extract the date range picker and enable the quick picker --- fitnesstrax/app/src/components/date_field.rs | 13 +- fitnesstrax/app/src/components/date_range.rs | 168 +++++++++++++++++++ fitnesstrax/app/src/components/mod.rs | 3 + fitnesstrax/app/src/views/historical_view.rs | 43 +---- 4 files changed, 184 insertions(+), 43 deletions(-) create mode 100644 fitnesstrax/app/src/components/date_range.rs diff --git a/fitnesstrax/app/src/components/date_field.rs b/fitnesstrax/app/src/components/date_field.rs index 7b56330..dafab5d 100644 --- a/fitnesstrax/app/src/components/date_field.rs +++ b/fitnesstrax/app/src/components/date_field.rs @@ -126,15 +126,18 @@ impl DateField { s.append(&s.imp().month.widget()); s.append(&s.imp().day.widget()); - s.imp().year.set_value(Some(date.year())); - s.imp().month.set_value(Some(date.month())); - s.imp().day.set_value(Some(date.day())); - - *s.imp().date.borrow_mut() = date; + s.set_date(date); s } + pub fn set_date(&self, date: chrono::NaiveDate) { + self.imp().year.set_value(Some(date.year())); + self.imp().month.set_value(Some(date.month())); + self.imp().day.set_value(Some(date.day())); + + *self.imp().date.borrow_mut() = date; + } pub fn date(&self) -> chrono::NaiveDate { self.imp().date.borrow().clone() diff --git a/fitnesstrax/app/src/components/date_range.rs b/fitnesstrax/app/src/components/date_range.rs new file mode 100644 index 0000000..ebad6aa --- /dev/null +++ b/fitnesstrax/app/src/components/date_range.rs @@ -0,0 +1,168 @@ +/* +Copyright 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 crate::{ + app::App, + components::{DateField, DaySummary}, + types::DayInterval, + view_models::DayDetailViewModel, +}; +use chrono::{Duration, Local, Months}; +use glib::Object; +use gtk::{prelude::*, subclass::prelude::*}; +use std::{cell::RefCell, rc::Rc}; + +type OnSearch = dyn Fn(DayInterval) + 'static; + +pub struct DateRangePickerPrivate { + start: DateField, + end: DateField, + + on_search: RefCell>, +} + +#[glib::object_subclass] +impl ObjectSubclass for DateRangePickerPrivate { + const NAME: &'static str = "DateRangePicker"; + type Type = DateRangePicker; + type ParentType = gtk::Box; + + fn new() -> Self { + let default_date = Local::now().date_naive(); + Self { + start: DateField::new(default_date), + end: DateField::new(default_date), + on_search: RefCell::new(Box::new(|_| {})), + } + } +} + +impl ObjectImpl for DateRangePickerPrivate {} +impl WidgetImpl for DateRangePickerPrivate {} +impl BoxImpl for DateRangePickerPrivate {} + +glib::wrapper! { + pub struct DateRangePicker(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; +} + +impl DateRangePicker { + pub fn new() -> Self { + let s: Self = Object::builder().build(); + s.set_orientation(gtk::Orientation::Vertical); + + let search_button = gtk::Button::with_label("Search"); + 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(); + last_week_button.connect_clicked({ + let s = s.clone(); + move |_| { + let end = Local::now().date_naive(); + let start = end - Duration::days(7); + s.set_interval(start, end); + (s.imp().on_search.borrow())(s.interval()); + } + }); + + let two_weeks_button = gtk::Button::builder().label("last two weeks").build(); + two_weeks_button.connect_clicked({ + let s = s.clone(); + move |_| { + let end = Local::now().date_naive(); + let start = end - Duration::days(14); + s.set_interval(start, end); + (s.imp().on_search.borrow())(s.interval()); + } + }); + + let last_month_button = gtk::Button::builder().label("last month").build(); + last_month_button.connect_clicked({ + let s = s.clone(); + move |_| { + let end = Local::now().date_naive(); + let start = end - Months::new(1); + s.set_interval(start, end); + (s.imp().on_search.borrow())(s.interval()); + } + }); + + let six_months_button = gtk::Button::builder().label("last six months").build(); + six_months_button.connect_clicked({ + let s = s.clone(); + move |_| { + let end = Local::now().date_naive(); + let start = end - Months::new(6); + s.set_interval(start, end); + (s.imp().on_search.borrow())(s.interval()); + } + }); + + let last_year_button = gtk::Button::builder().label("last year").build(); + last_year_button.connect_clicked({ + let s = s.clone(); + move |_| { + let end = Local::now().date_naive(); + let start = end - Months::new(12); + s.set_interval(start, end); + (s.imp().on_search.borrow())(s.interval()); + } + }); + + let date_row = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .build(); + date_row.append(&s.imp().start); + date_row.append(>k::Label::new(Some("to"))); + date_row.append(&s.imp().end); + date_row.append(&search_button); + + let quick_picker = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .build(); + quick_picker.append(&last_week_button); + quick_picker.append(&two_weeks_button); + quick_picker.append(&last_month_button); + quick_picker.append(&six_months_button); + quick_picker.append(&last_year_button); + + s.append(&date_row); + s.append(&quick_picker); + + s + } + + pub fn connect_on_search(&self, f: OnSearch) + where + OnSearch: Fn(DayInterval) + 'static, + { + *self.imp().on_search.borrow_mut() = Box::new(f); + } + + 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 { + DayInterval { + start: self.imp().start.date(), + end: self.imp().end.date(), + } + } +} diff --git a/fitnesstrax/app/src/components/mod.rs b/fitnesstrax/app/src/components/mod.rs index addb82f..d08f670 100644 --- a/fitnesstrax/app/src/components/mod.rs +++ b/fitnesstrax/app/src/components/mod.rs @@ -24,6 +24,9 @@ pub use day::{DayDetail, DayEdit, DaySummary}; mod date_field; pub use date_field::DateField; +mod date_range; +pub use date_range::DateRangePicker; + mod singleton; pub use singleton::{Singleton, SingletonImpl}; diff --git a/fitnesstrax/app/src/views/historical_view.rs b/fitnesstrax/app/src/views/historical_view.rs index 7bda8ea..bfb60e1 100644 --- a/fitnesstrax/app/src/views/historical_view.rs +++ b/fitnesstrax/app/src/views/historical_view.rs @@ -16,11 +16,10 @@ You should have received a copy of the GNU General Public License along with Fit use crate::{ app::App, - components::{DateField, DaySummary}, + components::{DateRangePicker, DaySummary}, types::DayInterval, view_models::DayDetailViewModel, }; - use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::{cell::RefCell, rc::Rc}; @@ -109,39 +108,12 @@ 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 date_range_picker = DateRangePicker::new(); + date_range_picker.connect_on_search({ 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); - } + move |interval| s.set_interval(interval) }); - let date_row = gtk::Box::builder() - .orientation(gtk::Orientation::Horizontal) - .build(); - date_row.append(&start_date); - date_row.append(>k::Label::new(Some("to"))); - date_row.append(&end_date); - date_row.append(&search_button); - - let quick_picker = gtk::Box::builder() - .orientation(gtk::Orientation::Horizontal) - .build(); - quick_picker.append(>k::Button::builder().label("last week").build()); - quick_picker.append(>k::Button::builder().label("last two weeks").build()); - quick_picker.append(>k::Button::builder().label("last month").build()); - quick_picker.append(>k::Button::builder().label("last six months").build()); - quick_picker.append(>k::Button::builder().label("last year").build()); - let mut model = gio::ListStore::new::(); model.extend(interval.days().map(Date::new)); s.imp() @@ -158,8 +130,7 @@ impl HistoricalView { } }); - s.append(&date_row); - s.append(&quick_picker); + s.append(&date_range_picker); s.append(&s.imp().list_view); s @@ -172,10 +143,6 @@ impl HistoricalView { .list_view .set_model(Some(>k::NoSelection::new(Some(model)))); } - - pub fn time_window(&self) -> DayInterval { - self.imp().time_window.borrow().clone() - } } #[derive(Default)] -- 2.44.1 From 605d310c57436b85f02ff1598204a4d2f5bfa898 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sun, 18 Feb 2024 11:28:40 -0500 Subject: [PATCH 5/7] Apply clippy --- fitnesstrax/app/src/components/date_field.rs | 9 +--- fitnesstrax/app/src/components/date_range.rs | 50 ++++++++++---------- fitnesstrax/app/src/components/text_entry.rs | 4 -- fitnesstrax/app/src/views/historical_view.rs | 4 +- 4 files changed, 28 insertions(+), 39 deletions(-) diff --git a/fitnesstrax/app/src/components/date_field.rs b/fitnesstrax/app/src/components/date_field.rs index dafab5d..7e8697c 100644 --- a/fitnesstrax/app/src/components/date_field.rs +++ b/fitnesstrax/app/src/components/date_field.rs @@ -20,13 +20,6 @@ use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::{cell::RefCell, rc::Rc}; -#[derive(Clone, Debug)] -enum Part { - Year, - Month, - Day, -} - pub struct DateFieldPrivate { date: Rc>, year: TextEntry, @@ -140,7 +133,7 @@ impl DateField { } pub fn date(&self) -> chrono::NaiveDate { - self.imp().date.borrow().clone() + *self.imp().date.borrow() } /* pub fn is_valid(&self) -> bool { diff --git a/fitnesstrax/app/src/components/date_range.rs b/fitnesstrax/app/src/components/date_range.rs index ebad6aa..6cb0198 100644 --- a/fitnesstrax/app/src/components/date_range.rs +++ b/fitnesstrax/app/src/components/date_range.rs @@ -15,15 +15,13 @@ You should have received a copy of the GNU General Public License along with Fit */ use crate::{ - app::App, - components::{DateField, DaySummary}, + components::{DateField}, types::DayInterval, - view_models::DayDetailViewModel, }; use chrono::{Duration, Local, Months}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; -use std::{cell::RefCell, rc::Rc}; +use std::{cell::RefCell}; type OnSearch = dyn Fn(DayInterval) + 'static; @@ -58,8 +56,31 @@ glib::wrapper! { pub struct DateRangePicker(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; } + impl DateRangePicker { - pub fn new() -> Self { + pub fn connect_on_search(&self, f: OnSearch) + where + OnSearch: Fn(DayInterval) + 'static, + { + *self.imp().on_search.borrow_mut() = Box::new(f); + } + + 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 { + DayInterval { + start: self.imp().start.date(), + end: self.imp().end.date(), + } + } +} + + +impl Default for DateRangePicker { + fn default() -> Self { let s: Self = Object::builder().build(); s.set_orientation(gtk::Orientation::Vertical); @@ -146,23 +167,4 @@ impl DateRangePicker { s } - - pub fn connect_on_search(&self, f: OnSearch) - where - OnSearch: Fn(DayInterval) + 'static, - { - *self.imp().on_search.borrow_mut() = Box::new(f); - } - - 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 { - DayInterval { - start: self.imp().start.date(), - end: self.imp().end.date(), - } - } } diff --git a/fitnesstrax/app/src/components/text_entry.rs b/fitnesstrax/app/src/components/text_entry.rs index 74615ab..a0790bc 100644 --- a/fitnesstrax/app/src/components/text_entry.rs +++ b/fitnesstrax/app/src/components/text_entry.rs @@ -109,10 +109,6 @@ impl TextEntry { 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::() } diff --git a/fitnesstrax/app/src/views/historical_view.rs b/fitnesstrax/app/src/views/historical_view.rs index bfb60e1..d843482 100644 --- a/fitnesstrax/app/src/views/historical_view.rs +++ b/fitnesstrax/app/src/views/historical_view.rs @@ -29,7 +29,6 @@ use std::{cell::RefCell, rc::Rc}; /// records. pub struct HistoricalViewPrivate { app: Rc>>, - time_window: Rc>, list_view: gtk::ListView, } @@ -50,7 +49,6 @@ impl ObjectSubclass for HistoricalViewPrivate { let s = Self { app: Rc::new(RefCell::new(None)), - time_window: Rc::new(RefCell::new(DayInterval::default())), list_view: gtk::ListView::builder() .factory(&factory) .single_click_activate(true) @@ -108,7 +106,7 @@ impl HistoricalView { *s.imp().app.borrow_mut() = Some(app); - let date_range_picker = DateRangePicker::new(); + let date_range_picker = DateRangePicker::default(); date_range_picker.connect_on_search({ let s = s.clone(); move |interval| s.set_interval(interval) -- 2.44.1 From 93667d04d6e4c858a3b8c66c0ccc9e05140f5a9d Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sun, 18 Feb 2024 16:59:14 -0500 Subject: [PATCH 6/7] Change TextEntry to a builder pattern --- fitnesstrax/app/src/components/date_field.rs | 45 ++-- fitnesstrax/app/src/components/mod.rs | 2 +- fitnesstrax/app/src/components/steps.rs | 18 +- fitnesstrax/app/src/components/text_entry.rs | 258 ++++++++++++------- 4 files changed, 202 insertions(+), 121 deletions(-) diff --git a/fitnesstrax/app/src/components/date_field.rs b/fitnesstrax/app/src/components/date_field.rs index 7e8697c..52ffccf 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::{i32_field, TextEntry, month_field}, types::ParseError}; +use crate::{components::{i32_field_builder, TextEntry, month_field_builder}, types::ParseError}; use chrono::{Datelike, Local}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; @@ -36,7 +36,9 @@ impl ObjectSubclass for DateFieldPrivate { fn new() -> Self { let date = Rc::new(RefCell::new(Local::now().date_naive())); - let year = i32_field(date.borrow().year(), + let year = i32_field_builder() + .with_value(date.borrow().year()) + .with_on_update( { let date = date.clone(); move |value| { @@ -47,13 +49,13 @@ impl ObjectSubclass for DateFieldPrivate { } } } - }, - ); - year.widget.set_max_length(4); - year.add_css_class("date-field__year"); + }) + .with_length(4) + .with_css_classes(vec!["date-field__year".to_owned()]).build(); - let month = month_field( - date.borrow().month(), + let month = month_field_builder() + .with_value(date.borrow().month()) + .with_on_update( { let date = date.clone(); move |value| { @@ -64,17 +66,17 @@ impl ObjectSubclass for DateFieldPrivate { } } } - }, - ); - month.add_css_class("date-field__month"); + }) + .with_css_classes(vec!["date-field__month".to_owned()]) + .build(); /* Modify this so that it enforces the number of days per month */ - let day = TextEntry::new( - "day", - Some(date.borrow().day()), - |v| format!("{}", v), - |v| v.parse::().map_err(|_| ParseError), - { + let day = TextEntry::builder() + .with_placeholder("day".to_owned()) + .with_value(date.borrow().day()) + .with_renderer(|v| format!("{}", v)) + .with_parser(|v| v.parse::().map_err(|_| ParseError)) + .with_on_update({ let date = date.clone(); move |value| { if let Some(day) = value { @@ -84,10 +86,9 @@ impl ObjectSubclass for DateFieldPrivate { } } } - }, - - ); - day.add_css_class("date-field__day"); + }) + .with_css_classes(vec!["date-field__day".to_owned()]) + .build(); Self { date, @@ -156,7 +157,7 @@ mod test { 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()); + // assert!(field.is_valid()); } #[test] diff --git a/fitnesstrax/app/src/components/mod.rs b/fitnesstrax/app/src/components/mod.rs index d08f670..3496e9b 100644 --- a/fitnesstrax/app/src/components/mod.rs +++ b/fitnesstrax/app/src/components/mod.rs @@ -34,7 +34,7 @@ mod steps; pub use steps::{steps_editor, Steps}; mod text_entry; -pub use text_entry::{distance_field, duration_field, time_field, weight_field, i32_field, month_field, TextEntry}; +pub use text_entry::{distance_field, duration_field, time_field, weight_field, i32_field_builder, month_field_builder, TextEntry}; mod time_distance; pub use time_distance::{time_distance_detail, time_distance_summary}; diff --git a/fitnesstrax/app/src/components/steps.rs b/fitnesstrax/app/src/components/steps.rs index 11ba591..b408247 100644 --- a/fitnesstrax/app/src/components/steps.rs +++ b/fitnesstrax/app/src/components/steps.rs @@ -46,11 +46,15 @@ pub fn steps_editor(value: Option, on_update: OnUpdate) -> TextEn where OnUpdate: Fn(Option) + 'static, { - TextEntry::new( - "0", - value, - |v| format!("{}", v), - |v| v.parse::().map_err(|_| ParseError), - on_update, - ) + let text_entry = TextEntry::builder() + .with_placeholder( "0".to_owned()) + .with_renderer(|v| format!("{}", v)) + .with_parser(|v| v.parse::().map_err(|_| ParseError)) + .with_on_update(on_update); + + if let Some(time) = value { + text_entry.with_value(time) + } else { + text_entry + }.build() } diff --git a/fitnesstrax/app/src/components/text_entry.rs b/fitnesstrax/app/src/components/text_entry.rs index a0790bc..3922a71 100644 --- a/fitnesstrax/app/src/components/text_entry.rs +++ b/fitnesstrax/app/src/components/text_entry.rs @@ -28,7 +28,7 @@ pub type OnUpdate = dyn Fn(Option); pub struct TextEntry { value: Rc>>, - pub widget: gtk::Entry, + widget: gtk::Entry, renderer: Rc String>, parser: Rc>, on_update: Rc>, @@ -46,29 +46,20 @@ impl std::fmt::Debug for TextEntry { // I do not understand why the data should be 'static. impl TextEntry { - pub fn new( - placeholder: &str, - value: Option, - renderer: R, - parser: V, - on_update: U, - ) -> Self - where - R: Fn(&T) -> String + 'static, - V: Fn(&str) -> Result + 'static, - U: Fn(Option) + 'static, - { - let widget = gtk::Entry::builder().placeholder_text(placeholder).build(); - if let Some(ref v) = value { - widget.set_text(&renderer(v)) + fn from_builder(builder: TextEntryBuilder) -> TextEntry { + let widget = gtk::Entry::builder() + .placeholder_text(builder.placeholder) + .build(); + if let Some(ref v) = builder.value { + widget.set_text(&(builder.renderer)(v)) } let s = Self { - value: Rc::new(RefCell::new(value)), + value: Rc::new(RefCell::new(builder.value)), widget, - renderer: Rc::new(renderer), - parser: Rc::new(parser), - on_update: Rc::new(on_update), + renderer: Rc::new(builder.renderer), + parser: Rc::new(builder.parser), + on_update: Rc::new(builder.on_update), }; s.widget.buffer().connect_text_notify({ @@ -76,9 +67,21 @@ impl TextEntry { move |buffer| s.handle_text_change(buffer) }); + if let Some(length) = builder.length { + s.widget.set_max_length(length.try_into().unwrap()); + } + + // let classes: Vec<&str> = builder.css_classes.iter(|v| v.as_ref()).collect(); + let classes: Vec<&str> = builder.css_classes.iter().map(AsRef::as_ref).collect(); + s.widget.set_css_classes(&classes); + s } + pub fn builder() -> TextEntryBuilder { + TextEntryBuilder::default() + } + pub fn set_value(&self, val: Option) { if let Some(ref v) = val { self.widget.set_text(&(self.renderer)(v)); @@ -105,10 +108,6 @@ impl TextEntry { } } - pub fn add_css_class(&self, class_: &str) { - self.widget.add_css_class(class_); - } - pub fn widget(&self) -> gtk::Widget { self.widget.clone().upcast::() } @@ -119,7 +118,85 @@ impl TextEntry { } } -#[allow(unused)] +pub struct TextEntryBuilder { + placeholder: String, + value: Option, + length: Option, + css_classes: Vec, + renderer: Box String>, + parser: Box>, + on_update: Box>, +} + +impl Default for TextEntryBuilder { + fn default() -> TextEntryBuilder { + TextEntryBuilder { + placeholder: "".to_owned(), + value: None, + length: None, + css_classes: vec![], + renderer: Box::new(|_| "".to_owned()), + parser: Box::new(|_| Err(ParseError)), + on_update: Box::new(|_| {}), + } + } +} + +impl TextEntryBuilder { + pub fn build(self) -> TextEntry { + TextEntry::from_builder(self) + } + + pub fn with_placeholder(self, placeholder: String) -> Self { + Self { + placeholder, + ..self + } + } + + pub fn with_value(self, value: T) -> Self { + Self { + value: Some(value), + ..self + } + } + + pub fn with_length(self, length: usize) -> Self { + Self { + length: Some(length), + ..self + } + } + + pub fn with_css_classes(self, classes: Vec) -> Self { + Self { + css_classes: classes, + ..self + } + } + + pub fn with_renderer(self, renderer: impl Fn(&T) -> String + 'static) -> Self { + Self { + renderer: Box::new(renderer), + ..self + } + } + + pub fn with_parser(self, parser: impl Fn(&str) -> Result + 'static) -> Self { + Self { + parser: Box::new(parser), + ..self + } + } + + pub fn with_on_update(self, on_update: impl Fn(Option) + 'static) -> Self { + Self { + on_update: Box::new(on_update), + ..self + } + } +} + pub fn time_field( value: Option, on_update: OnUpdate, @@ -127,16 +204,20 @@ pub fn time_field( where OnUpdate: Fn(Option) + 'static, { - TextEntry::new( - "HH:MM", - value, - |val| val.format(FormatOption::Abbreviated), - TimeFormatter::parse, - on_update, - ) + let text_entry = TextEntry::builder() + .with_placeholder("HH:MM".to_owned()) + .with_renderer(|val: &TimeFormatter| val.format(FormatOption::Abbreviated)) + .with_parser(TimeFormatter::parse) + .with_on_update(on_update); + + if let Some(time) = value { + text_entry.with_value(time) + } else { + text_entry + } + .build() } -#[allow(unused)] pub fn distance_field( value: Option, on_update: OnUpdate, @@ -144,16 +225,20 @@ pub fn distance_field( where OnUpdate: Fn(Option) + 'static, { - TextEntry::new( - "0 km", - value, - |val| val.format(FormatOption::Abbreviated), - DistanceFormatter::parse, - on_update, - ) + let text_entry = TextEntry::builder() + .with_placeholder("0 km".to_owned()) + .with_renderer(|val: &DistanceFormatter| val.format(FormatOption::Abbreviated)) + .with_parser(DistanceFormatter::parse) + .with_on_update(on_update); + + if let Some(distance) = value { + text_entry.with_value(distance) + } else { + text_entry + } + .build() } -#[allow(unused)] pub fn duration_field( value: Option, on_update: OnUpdate, @@ -161,13 +246,18 @@ pub fn duration_field( where OnUpdate: Fn(Option) + 'static, { - TextEntry::new( - "0 m", - value, - |val| val.format(FormatOption::Abbreviated), - DurationFormatter::parse, - on_update, - ) + let text_entry = TextEntry::builder() + .with_placeholder("0 m".to_owned()) + .with_renderer(|val: &DurationFormatter| val.format(FormatOption::Abbreviated)) + .with_parser(DurationFormatter::parse) + .with_on_update(on_update); + + if let Some(duration) = value { + text_entry.with_value(duration) + } else { + text_entry + } + .build() } pub fn weight_field( weight: Option, @@ -176,52 +266,39 @@ pub fn weight_field( where OnUpdate: Fn(Option) + 'static, { - TextEntry::new( - "0 kg", - weight, - |val| val.format(FormatOption::Abbreviated), - WeightFormatter::parse, - on_update, - ) + let text_entry = TextEntry::builder() + .with_placeholder("0 kg".to_owned()) + .with_renderer(|val: &WeightFormatter| val.format(FormatOption::Abbreviated)) + .with_parser(WeightFormatter::parse) + .with_on_update(on_update); + if let Some(weight) = weight { + text_entry.with_value(weight) + } else { + text_entry + } + .build() } -pub fn i32_field( - value: i32, - on_update: OnUpdate, -) -> TextEntry -where - OnUpdate: Fn(Option) + 'static, +pub fn i32_field_builder() -> TextEntryBuilder { - TextEntry::new( - "0", - Some(value), - |val| format!("{}", val), - |v| - v.parse::().map_err(|_| ParseError), - on_update, - ) + TextEntry::builder() + .with_placeholder("0".to_owned()) + .with_renderer(|val| format!("{}", val)) + .with_parser(|v| v.parse::().map_err(|_| ParseError)) } -pub fn month_field( - value: u32, - on_update: OnUpdate, -) -> TextEntry -where - OnUpdate: Fn(Option) + 'static, +pub fn month_field_builder() -> TextEntryBuilder { - TextEntry::new( - "0", - Some(value), - |val| format!("{}", val), - |v| { + TextEntry::builder() + .with_placeholder("0".to_owned()) + .with_renderer(|val| format!("{}", val)) + .with_parser(|v| { let val = v.parse::().map_err(|_| ParseError)?; if val == 0 || val > 12 { return Err(ParseError); } Ok(val) - }, - on_update, - ) + }) } #[cfg(test)] @@ -232,16 +309,15 @@ mod test { fn setup_u32_entry() -> (Rc>>, TextEntry) { let current_value = Rc::new(RefCell::new(None)); - let entry = TextEntry::new( - "step count", - None, - |steps| format!("{}", steps), - |v| v.parse::().map_err(|_| ParseError), - { + let entry = TextEntry::builder() + .with_placeholder("step count".to_owned()) + .with_renderer(|steps| format!("{}", steps)) + .with_parser(|v| v.parse::().map_err(|_| ParseError)) + .with_on_update({ let current_value = current_value.clone(); move |v| *current_value.borrow_mut() = v - }, - ); + }) + .build(); (current_value, entry) } -- 2.44.1 From 540e512f5f370e37ee9739ce816fc789aebf95e2 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sun, 18 Feb 2024 17:19:07 -0500 Subject: [PATCH 7/7] Wrap the list view within a scrolled window --- fitnesstrax/app/src/views/historical_view.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/fitnesstrax/app/src/views/historical_view.rs b/fitnesstrax/app/src/views/historical_view.rs index d843482..02848ee 100644 --- a/fitnesstrax/app/src/views/historical_view.rs +++ b/fitnesstrax/app/src/views/historical_view.rs @@ -128,8 +128,15 @@ impl HistoricalView { } }); + let scroller = gtk::ScrolledWindow::builder() + .child(&s.imp().list_view) + .hexpand(true) + .vexpand(true) + .hscrollbar_policy(gtk::PolicyType::Never) + .build(); + s.append(&date_range_picker); - s.append(&s.imp().list_view); + s.append(&scroller); s } -- 2.44.1