From a25b76d230c0fb82bb677b936139f0127ec1f909 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sun, 31 Dec 2023 11:18:15 -0500 Subject: [PATCH 1/4] Create a validated text entry widget I move the weight edit view into the validated text entry widget, and I work on some of the unfortunate logic in the weight blur function. I've left behind a lot of breadcrumbs for things that still need to be done. --- fitnesstrax/app/src/components/day.rs | 75 +++++++++++++++----- fitnesstrax/app/src/components/mod.rs | 3 + fitnesstrax/app/src/components/text_entry.rs | 73 +++++++++++++++++++ 3 files changed, 134 insertions(+), 17 deletions(-) create mode 100644 fitnesstrax/app/src/components/text_entry.rs diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index 19ebda5..4692ac3 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -16,6 +16,7 @@ You should have received a copy of the GNU General Public License along with Fit // use chrono::NaiveDate; // use ft_core::TraxRecord; +use crate::components::{ParseError, TextEntry}; use chrono::{Local, NaiveDate}; use dimensioned::si; use emseries::Record; @@ -220,12 +221,23 @@ impl DayDetail { } } +#[derive(Clone, Copy, PartialEq)] +enum WeightViewMode { + View, + Edit, +} + pub struct WeightViewPrivate { date: RefCell, record: RefCell>, view: RefCell, - edit: RefCell, + edit: RefCell>>, current: RefCell, + mode: RefCell, + + // If I create a swappable component and don't make it a true GTK component, the way + // TextEntry ended up not a true GTK component, on_edit_finished will not need to be a + // RefCell. on_edit_finished: RefCell)>>, } @@ -236,7 +248,9 @@ impl Default for WeightViewPrivate { .halign(gtk::Align::Start) .can_focus(true) .build(); - let edit = gtk::Entry::builder().halign(gtk::Align::Start).build(); + let edit = TextEntry::>::new("weight", None, &|w: &str| { + w.parse::().map(|w| w * si::KG).map_err(|_| ParseError) + }); let current = view.clone(); @@ -246,6 +260,7 @@ impl Default for WeightViewPrivate { view: RefCell::new(view), edit: RefCell::new(edit), current: RefCell::new(current.upcast()), + mode: RefCell::new(WeightViewMode::View), on_edit_finished: RefCell::new(Box::new(|_| {})), } } @@ -307,16 +322,18 @@ impl WeightView { view.set_label("No weight recorded"); } } + *self.imp().mode.borrow_mut() = WeightViewMode::View; self.swap(view.clone().upcast()); } fn edit(&self) { let edit = self.imp().edit.borrow(); match *self.imp().record.borrow() { - Some(ref record) => edit.buffer().set_text(&format!("{:?}", record.weight)), - None => edit.buffer().set_text(""), + Some(ref record) => edit.set_value(Some(record.weight)), + None => edit.set_value(None), } - self.swap(edit.clone().upcast()); + *self.imp().mode.borrow_mut() = WeightViewMode::Edit; + self.swap(edit.widget()); edit.grab_focus(); } @@ -328,20 +345,44 @@ impl WeightView { } fn blur(&self) { - let edit = self.imp().edit.borrow(); - if *self.imp().current.borrow() == *edit { - let w = edit.buffer().text().parse::().unwrap(); - self.imp().on_edit_finished.borrow()(w * si::KG); + if *self.imp().mode.borrow() == WeightViewMode::Edit { + println!("on_blur"); + let weight = self.imp().edit.borrow().value(); - let mut record = self.imp().record.borrow_mut(); - match *record { - Some(ref mut record) => record.weight = w * si::KG, - None => { - *record = Some(Weight { - date: self.imp().date.borrow().clone(), - weight: w * si::KG, - }) + println!("new weight: {:?}", weight); + + // This has really turned into rubbish + // on_edit_finished needs to accept a full record now. + // needs to be possible to delete a record if the value is None + // it's hard to be sure whether I need the full record object or if I need to update + // it. I probably don't. I think I need to borrow it and call on_edit_finished with an + // updated version of it. + // on_edit_finished still doesn't have a way to support a delete operation + let record = match (self.imp().record.borrow().clone(), weight) { + // update an existing record + (Some(record), Some(weight)) => Some(Weight { + date: record.date, + weight, + }), + + // create a new record + (None, Some(weight)) => Some(Weight { + date: self.imp().date.borrow().clone(), + weight, + }), + + // do nothing or delete an existing record + (_, None) => None, + }; + + println!("updated record: {:?}", record); + + match record { + Some(record) => { + self.imp().on_edit_finished.borrow()(record.weight); + *self.imp().record.borrow_mut() = Some(record); } + None => {} } } self.view(); diff --git a/fitnesstrax/app/src/components/mod.rs b/fitnesstrax/app/src/components/mod.rs index c6bce7d..450fad8 100644 --- a/fitnesstrax/app/src/components/mod.rs +++ b/fitnesstrax/app/src/components/mod.rs @@ -17,6 +17,9 @@ You should have received a copy of the GNU General Public License along with Fit mod day; pub use day::{DayDetail, DaySummary}; +mod text_entry; +pub use text_entry::{ParseError, TextEntry}; + use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::{cell::RefCell, path::PathBuf, rc::Rc}; diff --git a/fitnesstrax/app/src/components/text_entry.rs b/fitnesstrax/app/src/components/text_entry.rs new file mode 100644 index 0000000..690e5d8 --- /dev/null +++ b/fitnesstrax/app/src/components/text_entry.rs @@ -0,0 +1,73 @@ +/* +Copyright 2023, 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 gtk::prelude::*; +use std::{cell::RefCell, rc::Rc}; + +pub struct ParseError; + +#[derive(Clone)] +pub struct TextEntry { + value: Rc>>, + widget: gtk::Entry, +} + +impl TextEntry { + pub fn new( + placeholder: &str, + value: Option, + validator: &'static dyn Fn(&str) -> Result, + ) -> Self { + let widget = gtk::Entry::builder().placeholder_text(placeholder).build(); + + let s = Self { + value: Rc::new(RefCell::new(value)), + widget, + }; + + s.widget.buffer().connect_text_notify({ + let s = s.clone(); + move |buffer| { + if buffer.text().is_empty() { + *s.value.borrow_mut() = None; + } + match validator(buffer.text().as_str()) { + Ok(v) => *s.value.borrow_mut() = Some(v), + // need to change the border to provide a visual indicator of an error + Err(_) => {} + } + } + }); + + s + } + + pub fn value(&self) -> Option { + self.value.borrow().clone() + } + + pub fn set_value(&self, value: Option) { + *self.value.borrow_mut() = value; + } + + pub fn grab_focus(&self) { + self.widget.grab_focus(); + } + + pub fn widget(&self) -> gtk::Widget { + self.widget.clone().upcast::() + } +} -- 2.44.1 From 2e3d5fc5a4e7a0c17370e0b14320df5373fa59be Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 1 Jan 2024 22:51:40 -0500 Subject: [PATCH 2/4] Clean up the parameters to TextEntry and populate the field --- fitnesstrax/app/src/components/day.rs | 9 ++-- fitnesstrax/app/src/components/text_entry.rs | 54 ++++++++++++++------ 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index 4692ac3..9e5a28b 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -248,9 +248,12 @@ impl Default for WeightViewPrivate { .halign(gtk::Align::Start) .can_focus(true) .build(); - let edit = TextEntry::>::new("weight", None, &|w: &str| { - w.parse::().map(|w| w * si::KG).map_err(|_| ParseError) - }); + let edit = TextEntry::>::new( + "weight", + None, + |w: &si::Kilogram| w.to_string(), + |w: &str| w.parse::().map(|w| w * si::KG).map_err(|_| ParseError), + ); let current = view.clone(); diff --git a/fitnesstrax/app/src/components/text_entry.rs b/fitnesstrax/app/src/components/text_entry.rs index 690e5d8..2220ec9 100644 --- a/fitnesstrax/app/src/components/text_entry.rs +++ b/fitnesstrax/app/src/components/text_entry.rs @@ -23,43 +23,65 @@ pub struct ParseError; pub struct TextEntry { value: Rc>>, widget: gtk::Entry, + renderer: Rc String>>, + validator: Rc Result>>, } -impl TextEntry { - pub fn new( - placeholder: &str, - value: Option, - validator: &'static dyn Fn(&str) -> Result, - ) -> Self { +// I do not understand why the data should be 'static. +impl TextEntry { + pub fn new(placeholder: &str, value: Option, renderer: R, validator: V) -> Self + where + R: Fn(&T) -> String + 'static, + V: Fn(&str) -> Result + 'static, + { let widget = gtk::Entry::builder().placeholder_text(placeholder).build(); + match value { + Some(ref v) => widget.set_text(&renderer(&v)), + None => {} + } let s = Self { value: Rc::new(RefCell::new(value)), widget, + renderer: Rc::new(Box::new(renderer)), + validator: Rc::new(Box::new(validator)), }; s.widget.buffer().connect_text_notify({ let s = s.clone(); - move |buffer| { - if buffer.text().is_empty() { - *s.value.borrow_mut() = None; - } - match validator(buffer.text().as_str()) { - Ok(v) => *s.value.borrow_mut() = Some(v), - // need to change the border to provide a visual indicator of an error - Err(_) => {} - } - } + move |buffer| s.handle_text_change(buffer) }); s } + fn handle_text_change(&self, buffer: >k::EntryBuffer) { + if buffer.text().is_empty() { + *self.value.borrow_mut() = None; + self.widget.remove_css_class("error"); + return; + } + match (self.validator)(buffer.text().as_str()) { + Ok(v) => { + *self.value.borrow_mut() = Some(v); + self.widget.remove_css_class("error"); + } + // need to change the border to provide a visual indicator of an error + Err(_) => { + self.widget.add_css_class("error"); + } + } + } + pub fn value(&self) -> Option { self.value.borrow().clone() } pub fn set_value(&self, value: Option) { + match value { + Some(ref v) => self.widget.set_text(&(self.renderer)(&v)), + None => {} + } *self.value.borrow_mut() = value; } -- 2.44.1 From b7b9b1b29f8fabbca073bbd80d867e41bd279b9f Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 1 Jan 2024 23:49:31 -0500 Subject: [PATCH 3/4] Create an EditView "component" This is a super tiny data structure that covers an edit mode, a view mode, and an unconfigured mode. It's mostly a container so that views don't have to preserve everything directly. --- fitnesstrax/app/src/components/day.rs | 156 ++++++++++---------- fitnesstrax/app/src/components/edit_view.rs | 22 +++ fitnesstrax/app/src/components/mod.rs | 3 + 3 files changed, 99 insertions(+), 82 deletions(-) create mode 100644 fitnesstrax/app/src/components/edit_view.rs diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index 9e5a28b..b16980d 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -16,7 +16,7 @@ You should have received a copy of the GNU General Public License along with Fit // use chrono::NaiveDate; // use ft_core::TraxRecord; -use crate::components::{ParseError, TextEntry}; +use crate::components::{EditView, ParseError, TextEntry}; use chrono::{Local, NaiveDate}; use dimensioned::si; use emseries::Record; @@ -221,49 +221,21 @@ impl DayDetail { } } -#[derive(Clone, Copy, PartialEq)] -enum WeightViewMode { - View, - Edit, -} - pub struct WeightViewPrivate { date: RefCell, record: RefCell>, - view: RefCell, - edit: RefCell>>, - current: RefCell, - mode: RefCell, - // If I create a swappable component and don't make it a true GTK component, the way - // TextEntry ended up not a true GTK component, on_edit_finished will not need to be a - // RefCell. + widget: RefCell>>>, + on_edit_finished: RefCell)>>, } impl Default for WeightViewPrivate { fn default() -> Self { - let view = gtk::Label::builder() - .css_classes(["card", "weight-view"]) - .halign(gtk::Align::Start) - .can_focus(true) - .build(); - let edit = TextEntry::>::new( - "weight", - None, - |w: &si::Kilogram| w.to_string(), - |w: &str| w.parse::().map(|w| w * si::KG).map_err(|_| ParseError), - ); - - let current = view.clone(); - Self { date: RefCell::new(Local::now().date_naive()), record: RefCell::new(None), - view: RefCell::new(view), - edit: RefCell::new(edit), - current: RefCell::new(current.upcast()), - mode: RefCell::new(WeightViewMode::View), + widget: RefCell::new(EditView::Unconfigured), on_edit_finished: RefCell::new(Box::new(|_| {})), } } @@ -301,20 +273,26 @@ impl WeightView { *s.imp().record.borrow_mut() = weight; s.view(); + s + } + + fn view(&self) { + let view = gtk::Label::builder() + .css_classes(["card", "weight-view"]) + .halign(gtk::Align::Start) + .can_focus(true) + .build(); + let view_click_controller = gtk::GestureClick::new(); view_click_controller.connect_released({ - let s = s.clone(); + let s = self.clone(); move |_, _, _, _| { s.edit(); } }); - s.imp().view.borrow().add_controller(view_click_controller); - s - } + view.add_controller(view_click_controller); - fn view(&self) { - let view = self.imp().view.borrow(); match *self.imp().record.borrow() { Some(ref record) => { view.remove_css_class("dim_label"); @@ -325,69 +303,83 @@ impl WeightView { view.set_label("No weight recorded"); } } - *self.imp().mode.borrow_mut() = WeightViewMode::View; - self.swap(view.clone().upcast()); + + self.swap(EditView::View(view)); } fn edit(&self) { - let edit = self.imp().edit.borrow(); + let edit = TextEntry::>::new( + "weight", + None, + |val: &si::Kilogram| val.to_string(), + |v: &str| v.parse::().map(|w| w * si::KG).map_err(|_| ParseError), + ); + match *self.imp().record.borrow() { Some(ref record) => edit.set_value(Some(record.weight)), None => edit.set_value(None), } - *self.imp().mode.borrow_mut() = WeightViewMode::Edit; - self.swap(edit.widget()); + + self.swap(EditView::Edit(edit.clone())); edit.grab_focus(); } - fn swap(&self, new_view: gtk::Widget) { - let mut current = self.imp().current.borrow_mut(); - self.remove(&*current); - self.append(&new_view); - *current = new_view; + fn swap(&self, new_view: EditView>>) { + let mut widget = self.imp().widget.borrow_mut(); + match *widget { + EditView::Unconfigured => {} + EditView::View(ref view) => self.remove(view), + EditView::Edit(ref editor) => self.remove(&editor.widget()), + } + + match new_view { + EditView::Unconfigured => {} + EditView::View(ref view) => self.append(view), + EditView::Edit(ref editor) => self.append(&editor.widget()), + } + *widget = new_view; } fn blur(&self) { - if *self.imp().mode.borrow() == WeightViewMode::Edit { - println!("on_blur"); - let weight = self.imp().edit.borrow().value(); + match *self.imp().widget.borrow() { + EditView::Unconfigured => {} + EditView::View(_) => {} + EditView::Edit(ref editor) => { + let weight = editor.value(); + // This has really turned into rubbish + // on_edit_finished needs to accept a full record now. + // needs to be possible to delete a record if the value is None + // it's hard to be sure whether I need the full record object or if I need to update + // it. I probably don't. I think I need to borrow it and call on_edit_finished with an + // updated version of it. + // on_edit_finished still doesn't have a way to support a delete operation + let record = match (self.imp().record.borrow().clone(), weight) { + // update an existing record + (Some(record), Some(weight)) => Some(Weight { + date: record.date, + weight, + }), - println!("new weight: {:?}", weight); + // create a new record + (None, Some(weight)) => Some(Weight { + date: self.imp().date.borrow().clone(), + weight, + }), - // This has really turned into rubbish - // on_edit_finished needs to accept a full record now. - // needs to be possible to delete a record if the value is None - // it's hard to be sure whether I need the full record object or if I need to update - // it. I probably don't. I think I need to borrow it and call on_edit_finished with an - // updated version of it. - // on_edit_finished still doesn't have a way to support a delete operation - let record = match (self.imp().record.borrow().clone(), weight) { - // update an existing record - (Some(record), Some(weight)) => Some(Weight { - date: record.date, - weight, - }), + // do nothing or delete an existing record + (_, None) => None, + }; - // create a new record - (None, Some(weight)) => Some(Weight { - date: self.imp().date.borrow().clone(), - weight, - }), - - // do nothing or delete an existing record - (_, None) => None, - }; - - println!("updated record: {:?}", record); - - match record { - Some(record) => { - self.imp().on_edit_finished.borrow()(record.weight); - *self.imp().record.borrow_mut() = Some(record); + match record { + Some(record) => { + self.imp().on_edit_finished.borrow()(record.weight); + *self.imp().record.borrow_mut() = Some(record); + } + None => {} } - None => {} } } + self.view(); } } diff --git a/fitnesstrax/app/src/components/edit_view.rs b/fitnesstrax/app/src/components/edit_view.rs new file mode 100644 index 0000000..f6f96a9 --- /dev/null +++ b/fitnesstrax/app/src/components/edit_view.rs @@ -0,0 +1,22 @@ +/* +Copyright 2023, 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 . +*/ + +#[derive(Clone)] +pub enum EditView { + Unconfigured, + View(View), + Edit(Edit), +} diff --git a/fitnesstrax/app/src/components/mod.rs b/fitnesstrax/app/src/components/mod.rs index 450fad8..30e7908 100644 --- a/fitnesstrax/app/src/components/mod.rs +++ b/fitnesstrax/app/src/components/mod.rs @@ -17,6 +17,9 @@ You should have received a copy of the GNU General Public License along with Fit mod day; pub use day::{DayDetail, DaySummary}; +mod edit_view; +pub use edit_view::EditView; + mod text_entry; pub use text_entry::{ParseError, TextEntry}; -- 2.44.1 From 0007522b2636abc3436f9e0913508ea3000d36ef Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 1 Jan 2024 23:58:55 -0500 Subject: [PATCH 4/4] Extract the Weight and Time Distance widgets --- fitnesstrax/app/src/components/day.rs | 255 +----------------- fitnesstrax/app/src/components/mod.rs | 6 + .../app/src/components/time_distance.rs | 107 ++++++++ fitnesstrax/app/src/components/weight.rs | 186 +++++++++++++ 4 files changed, 302 insertions(+), 252 deletions(-) create mode 100644 fitnesstrax/app/src/components/time_distance.rs create mode 100644 fitnesstrax/app/src/components/weight.rs diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index b16980d..8735086 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -16,14 +16,12 @@ You should have received a copy of the GNU General Public License along with Fit // use chrono::NaiveDate; // use ft_core::TraxRecord; -use crate::components::{EditView, ParseError, TextEntry}; -use chrono::{Local, NaiveDate}; -use dimensioned::si; +use crate::components::{TimeDistanceView, WeightView}; use emseries::Record; -use ft_core::{RecordType, TimeDistance, TraxRecord, Weight}; +use ft_core::{RecordType, TraxRecord, Weight}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; -use std::{cell::RefCell, rc::Rc}; +use std::cell::RefCell; pub struct DaySummaryPrivate { date: gtk::Label, @@ -220,250 +218,3 @@ impl DayDetail { s } } - -pub struct WeightViewPrivate { - date: RefCell, - record: RefCell>, - - widget: RefCell>>>, - - on_edit_finished: RefCell)>>, -} - -impl Default for WeightViewPrivate { - fn default() -> Self { - Self { - date: RefCell::new(Local::now().date_naive()), - record: RefCell::new(None), - widget: RefCell::new(EditView::Unconfigured), - on_edit_finished: RefCell::new(Box::new(|_| {})), - } - } -} - -#[glib::object_subclass] -impl ObjectSubclass for WeightViewPrivate { - const NAME: &'static str = "WeightView"; - type Type = WeightView; - type ParentType = gtk::Box; -} - -impl ObjectImpl for WeightViewPrivate {} -impl WidgetImpl for WeightViewPrivate {} -impl BoxImpl for WeightViewPrivate {} - -glib::wrapper! { - pub struct WeightView(ObjectSubclass) @extends gtk::Box, gtk::Widget; -} - -impl WeightView { - pub fn new( - date: NaiveDate, - weight: Option, - on_edit_finished: OnEditFinished, - ) -> Self - where - OnEditFinished: Fn(si::Kilogram) + 'static, - { - let s: Self = Object::builder().build(); - - *s.imp().on_edit_finished.borrow_mut() = Box::new(on_edit_finished); - *s.imp().date.borrow_mut() = date; - - *s.imp().record.borrow_mut() = weight; - s.view(); - - s - } - - fn view(&self) { - let view = gtk::Label::builder() - .css_classes(["card", "weight-view"]) - .halign(gtk::Align::Start) - .can_focus(true) - .build(); - - let view_click_controller = gtk::GestureClick::new(); - view_click_controller.connect_released({ - let s = self.clone(); - move |_, _, _, _| { - s.edit(); - } - }); - - view.add_controller(view_click_controller); - - match *self.imp().record.borrow() { - Some(ref record) => { - view.remove_css_class("dim_label"); - view.set_label(&format!("{:?}", record.weight)); - } - None => { - view.add_css_class("dim_label"); - view.set_label("No weight recorded"); - } - } - - self.swap(EditView::View(view)); - } - - fn edit(&self) { - let edit = TextEntry::>::new( - "weight", - None, - |val: &si::Kilogram| val.to_string(), - |v: &str| v.parse::().map(|w| w * si::KG).map_err(|_| ParseError), - ); - - match *self.imp().record.borrow() { - Some(ref record) => edit.set_value(Some(record.weight)), - None => edit.set_value(None), - } - - self.swap(EditView::Edit(edit.clone())); - edit.grab_focus(); - } - - fn swap(&self, new_view: EditView>>) { - let mut widget = self.imp().widget.borrow_mut(); - match *widget { - EditView::Unconfigured => {} - EditView::View(ref view) => self.remove(view), - EditView::Edit(ref editor) => self.remove(&editor.widget()), - } - - match new_view { - EditView::Unconfigured => {} - EditView::View(ref view) => self.append(view), - EditView::Edit(ref editor) => self.append(&editor.widget()), - } - *widget = new_view; - } - - fn blur(&self) { - match *self.imp().widget.borrow() { - EditView::Unconfigured => {} - EditView::View(_) => {} - EditView::Edit(ref editor) => { - let weight = editor.value(); - // This has really turned into rubbish - // on_edit_finished needs to accept a full record now. - // needs to be possible to delete a record if the value is None - // it's hard to be sure whether I need the full record object or if I need to update - // it. I probably don't. I think I need to borrow it and call on_edit_finished with an - // updated version of it. - // on_edit_finished still doesn't have a way to support a delete operation - let record = match (self.imp().record.borrow().clone(), weight) { - // update an existing record - (Some(record), Some(weight)) => Some(Weight { - date: record.date, - weight, - }), - - // create a new record - (None, Some(weight)) => Some(Weight { - date: self.imp().date.borrow().clone(), - weight, - }), - - // do nothing or delete an existing record - (_, None) => None, - }; - - match record { - Some(record) => { - self.imp().on_edit_finished.borrow()(record.weight); - *self.imp().record.borrow_mut() = Some(record); - } - None => {} - } - } - } - - self.view(); - } -} - -#[derive(Default)] -pub struct TimeDistanceViewPrivate { - record: RefCell>, -} - -#[glib::object_subclass] -impl ObjectSubclass for TimeDistanceViewPrivate { - const NAME: &'static str = "TimeDistanceView"; - type Type = TimeDistanceView; - type ParentType = gtk::Box; -} - -impl ObjectImpl for TimeDistanceViewPrivate {} -impl WidgetImpl for TimeDistanceViewPrivate {} -impl BoxImpl for TimeDistanceViewPrivate {} - -glib::wrapper! { - pub struct TimeDistanceView(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; -} - -impl TimeDistanceView { - pub fn new(type_: RecordType, record: TimeDistance) -> Self { - let s: Self = Object::builder().build(); - s.set_orientation(gtk::Orientation::Vertical); - s.set_hexpand(true); - - let first_row = gtk::Box::builder().homogeneous(true).build(); - - first_row.append( - >k::Label::builder() - .halign(gtk::Align::Start) - .label(&record.datetime.format("%H:%M").to_string()) - .build(), - ); - - first_row.append( - >k::Label::builder() - .halign(gtk::Align::Start) - .label(format!("{:?}", type_)) - .build(), - ); - - first_row.append( - >k::Label::builder() - .halign(gtk::Align::Start) - .label( - record - .distance - .map(|dist| format!("{}", dist)) - .unwrap_or("".to_owned()), - ) - .build(), - ); - - first_row.append( - >k::Label::builder() - .halign(gtk::Align::Start) - .label( - record - .duration - .map(|duration| format!("{}", duration)) - .unwrap_or("".to_owned()), - ) - .build(), - ); - - s.append(&first_row); - - s.append( - >k::Label::builder() - .halign(gtk::Align::Start) - .label( - record - .comments - .map(|comments| format!("{}", comments)) - .unwrap_or("".to_owned()), - ) - .build(), - ); - - s - } -} diff --git a/fitnesstrax/app/src/components/mod.rs b/fitnesstrax/app/src/components/mod.rs index 30e7908..19ee548 100644 --- a/fitnesstrax/app/src/components/mod.rs +++ b/fitnesstrax/app/src/components/mod.rs @@ -23,6 +23,12 @@ pub use edit_view::EditView; mod text_entry; pub use text_entry::{ParseError, TextEntry}; +mod time_distance; +pub use time_distance::TimeDistanceView; + +mod weight; +pub use weight::WeightView; + use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::{cell::RefCell, path::PathBuf, rc::Rc}; diff --git a/fitnesstrax/app/src/components/time_distance.rs b/fitnesstrax/app/src/components/time_distance.rs new file mode 100644 index 0000000..8a20c42 --- /dev/null +++ b/fitnesstrax/app/src/components/time_distance.rs @@ -0,0 +1,107 @@ +/* +Copyright 2023, 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::components::{EditView, ParseError, TextEntry}; +// use chrono::{Local, NaiveDate}; +// use dimensioned::si; +use ft_core::{RecordType, TimeDistance}; +use glib::Object; +use gtk::{prelude::*, subclass::prelude::*}; +use std::cell::RefCell; + +#[derive(Default)] +pub struct TimeDistanceViewPrivate { + record: RefCell>, +} + +#[glib::object_subclass] +impl ObjectSubclass for TimeDistanceViewPrivate { + const NAME: &'static str = "TimeDistanceView"; + type Type = TimeDistanceView; + type ParentType = gtk::Box; +} + +impl ObjectImpl for TimeDistanceViewPrivate {} +impl WidgetImpl for TimeDistanceViewPrivate {} +impl BoxImpl for TimeDistanceViewPrivate {} + +glib::wrapper! { + pub struct TimeDistanceView(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; +} + +impl TimeDistanceView { + pub fn new(type_: RecordType, record: TimeDistance) -> Self { + let s: Self = Object::builder().build(); + s.set_orientation(gtk::Orientation::Vertical); + s.set_hexpand(true); + + let first_row = gtk::Box::builder().homogeneous(true).build(); + + first_row.append( + >k::Label::builder() + .halign(gtk::Align::Start) + .label(&record.datetime.format("%H:%M").to_string()) + .build(), + ); + + first_row.append( + >k::Label::builder() + .halign(gtk::Align::Start) + .label(format!("{:?}", type_)) + .build(), + ); + + first_row.append( + >k::Label::builder() + .halign(gtk::Align::Start) + .label( + record + .distance + .map(|dist| format!("{}", dist)) + .unwrap_or("".to_owned()), + ) + .build(), + ); + + first_row.append( + >k::Label::builder() + .halign(gtk::Align::Start) + .label( + record + .duration + .map(|duration| format!("{}", duration)) + .unwrap_or("".to_owned()), + ) + .build(), + ); + + s.append(&first_row); + + s.append( + >k::Label::builder() + .halign(gtk::Align::Start) + .label( + record + .comments + .map(|comments| format!("{}", comments)) + .unwrap_or("".to_owned()), + ) + .build(), + ); + + s + } +} diff --git a/fitnesstrax/app/src/components/weight.rs b/fitnesstrax/app/src/components/weight.rs new file mode 100644 index 0000000..f32d11e --- /dev/null +++ b/fitnesstrax/app/src/components/weight.rs @@ -0,0 +1,186 @@ +/* +Copyright 2023, 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::components::{EditView, ParseError, TextEntry}; +use chrono::{Local, NaiveDate}; +use dimensioned::si; +use ft_core::Weight; +use glib::Object; +use gtk::{prelude::*, subclass::prelude::*}; +use std::cell::RefCell; + +pub struct WeightViewPrivate { + date: RefCell, + record: RefCell>, + + widget: RefCell>>>, + + on_edit_finished: RefCell)>>, +} + +impl Default for WeightViewPrivate { + fn default() -> Self { + Self { + date: RefCell::new(Local::now().date_naive()), + record: RefCell::new(None), + widget: RefCell::new(EditView::Unconfigured), + on_edit_finished: RefCell::new(Box::new(|_| {})), + } + } +} + +#[glib::object_subclass] +impl ObjectSubclass for WeightViewPrivate { + const NAME: &'static str = "WeightView"; + type Type = WeightView; + type ParentType = gtk::Box; +} + +impl ObjectImpl for WeightViewPrivate {} +impl WidgetImpl for WeightViewPrivate {} +impl BoxImpl for WeightViewPrivate {} + +glib::wrapper! { + pub struct WeightView(ObjectSubclass) @extends gtk::Box, gtk::Widget; +} + +impl WeightView { + pub fn new( + date: NaiveDate, + weight: Option, + on_edit_finished: OnEditFinished, + ) -> Self + where + OnEditFinished: Fn(si::Kilogram) + 'static, + { + let s: Self = Object::builder().build(); + + *s.imp().on_edit_finished.borrow_mut() = Box::new(on_edit_finished); + *s.imp().date.borrow_mut() = date; + + *s.imp().record.borrow_mut() = weight; + s.view(); + + s + } + + fn view(&self) { + let view = gtk::Label::builder() + .css_classes(["card", "weight-view"]) + .halign(gtk::Align::Start) + .can_focus(true) + .build(); + + let view_click_controller = gtk::GestureClick::new(); + view_click_controller.connect_released({ + let s = self.clone(); + move |_, _, _, _| { + s.edit(); + } + }); + + view.add_controller(view_click_controller); + + match *self.imp().record.borrow() { + Some(ref record) => { + view.remove_css_class("dim_label"); + view.set_label(&format!("{:?}", record.weight)); + } + None => { + view.add_css_class("dim_label"); + view.set_label("No weight recorded"); + } + } + + self.swap(EditView::View(view)); + } + + fn edit(&self) { + let edit = TextEntry::>::new( + "weight", + None, + |val: &si::Kilogram| val.to_string(), + |v: &str| v.parse::().map(|w| w * si::KG).map_err(|_| ParseError), + ); + + match *self.imp().record.borrow() { + Some(ref record) => edit.set_value(Some(record.weight)), + None => edit.set_value(None), + } + + self.swap(EditView::Edit(edit.clone())); + edit.grab_focus(); + } + + fn swap(&self, new_view: EditView>>) { + let mut widget = self.imp().widget.borrow_mut(); + match *widget { + EditView::Unconfigured => {} + EditView::View(ref view) => self.remove(view), + EditView::Edit(ref editor) => self.remove(&editor.widget()), + } + + match new_view { + EditView::Unconfigured => {} + EditView::View(ref view) => self.append(view), + EditView::Edit(ref editor) => self.append(&editor.widget()), + } + *widget = new_view; + } + + pub fn blur(&self) { + match *self.imp().widget.borrow() { + EditView::Unconfigured => {} + EditView::View(_) => {} + EditView::Edit(ref editor) => { + let weight = editor.value(); + // This has really turned into rubbish + // on_edit_finished needs to accept a full record now. + // needs to be possible to delete a record if the value is None + // it's hard to be sure whether I need the full record object or if I need to update + // it. I probably don't. I think I need to borrow it and call on_edit_finished with an + // updated version of it. + // on_edit_finished still doesn't have a way to support a delete operation + let record = match (self.imp().record.borrow().clone(), weight) { + // update an existing record + (Some(record), Some(weight)) => Some(Weight { + date: record.date, + weight, + }), + + // create a new record + (None, Some(weight)) => Some(Weight { + date: self.imp().date.borrow().clone(), + weight, + }), + + // do nothing or delete an existing record + (_, None) => None, + }; + + match record { + Some(record) => { + self.imp().on_edit_finished.borrow()(record.weight); + *self.imp().record.borrow_mut() = Some(record); + } + None => {} + } + } + } + + self.view(); + } +} -- 2.44.1