diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index b4b130d..b552d56 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -93,7 +93,7 @@ impl DaySummary { .css_classes(["day-summary__weight"]) .build(); if let Some(s) = view_model.steps() { - label.set_label(&format!("{} steps", s.to_string())); + label.set_label(&format!("{} steps", s)); } row.append(&label); @@ -285,8 +285,9 @@ impl DayEdit { top_row.append( &weight_editor(view_model.weight(), { let view_model = view_model.clone(); - move |w| { - view_model.set_weight(w); + move |w| match w { + Some(w) => view_model.set_weight(w), + None => eprintln!("have not implemented record delete"), } }) .widget(), @@ -295,7 +296,10 @@ impl DayEdit { top_row.append( &steps_editor(view_model.steps(), { let view_model = view_model.clone(); - move |s| view_model.set_steps(s) + move |s| match s { + Some(s) => view_model.set_steps(s), + None => eprintln!("have not implemented record delete"), + } }) .widget(), ); diff --git a/fitnesstrax/app/src/components/steps.rs b/fitnesstrax/app/src/components/steps.rs index e81c840..a23828f 100644 --- a/fitnesstrax/app/src/components/steps.rs +++ b/fitnesstrax/app/src/components/steps.rs @@ -44,18 +44,13 @@ impl Steps { pub fn steps_editor(value: Option, on_update: OnUpdate) -> TextEntry where - OnUpdate: Fn(u32) + 'static, + OnUpdate: Fn(Option) + 'static, { TextEntry::new( "0", value, |v| format!("{}", v), - move |v| match v.parse::() { - Ok(val) => { - on_update(val); - Ok(val) - } - Err(_) => Err(ParseError), - }, + move |v| v.parse::().map_err(|_| ParseError), + on_update, ) } diff --git a/fitnesstrax/app/src/components/text_entry.rs b/fitnesstrax/app/src/components/text_entry.rs index 12c0a6c..6dd1475 100644 --- a/fitnesstrax/app/src/components/text_entry.rs +++ b/fitnesstrax/app/src/components/text_entry.rs @@ -1,5 +1,5 @@ /* -Copyright 2023, Savanni D'Gerinel +Copyright 2023-2024, Savanni D'Gerinel This file is part of FitnessTrax. @@ -20,16 +20,16 @@ use std::{cell::RefCell, rc::Rc}; #[derive(Clone, Debug)] pub struct ParseError; -type Renderer = dyn Fn(&T) -> String; type Parser = dyn Fn(&str) -> Result; +type OnUpdate = dyn Fn(Option); #[derive(Clone)] pub struct TextEntry { value: Rc>>, + widget: gtk::Entry, - #[allow(unused)] - renderer: Rc>, parser: Rc>, + on_update: Rc>, } impl std::fmt::Debug for TextEntry { @@ -44,10 +44,17 @@ 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) -> Self + 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 { @@ -57,8 +64,8 @@ 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), }; s.widget.buffer().connect_text_notify({ @@ -73,12 +80,14 @@ impl TextEntry { if buffer.text().is_empty() { *self.value.borrow_mut() = None; self.widget.remove_css_class("error"); + (self.on_update)(None); return; } match (self.parser)(buffer.text().as_str()) { Ok(v) => { - *self.value.borrow_mut() = Some(v); + *self.value.borrow_mut() = Some(v.clone()); self.widget.remove_css_class("error"); + (self.on_update)(Some(v)); } // need to change the border to provide a visual indicator of an error Err(_) => { @@ -87,25 +96,84 @@ impl TextEntry { } } - #[allow(unused)] - pub fn value(&self) -> Option { - let v = self.value.borrow().clone(); - self.value.borrow().clone() - } - - pub fn set_value(&self, value: Option) { - if let Some(ref v) = value { - self.widget.set_text(&(self.renderer)(v)) - } - *self.value.borrow_mut() = value; - } - - #[allow(unused)] - pub fn grab_focus(&self) { - self.widget.grab_focus(); - } - pub fn widget(&self) -> gtk::Widget { self.widget.clone().upcast::() } + + #[cfg(test)] + fn has_parse_error(&self) -> bool { + self.widget.has_css_class("error") + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::gtk_init::gtk_init; + + 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 current_value = current_value.clone(); + move |v| *current_value.borrow_mut() = v + }, + ); + + (current_value, entry) + } + + #[test] + fn it_responds_to_field_changes() { + gtk_init(); + let (current_value, entry) = setup_u32_entry(); + let buffer = entry.widget.buffer(); + + buffer.set_text("1"); + assert_eq!(*current_value.borrow(), Some(1)); + + buffer.set_text("15"); + assert_eq!(*current_value.borrow(), Some(15)); + } + + #[test] + fn it_preserves_last_value_in_parse_error() { + crate::gtk_init::gtk_init(); + let (current_value, entry) = setup_u32_entry(); + let buffer = entry.widget.buffer(); + + buffer.set_text("1"); + assert_eq!(*current_value.borrow(), Some(1)); + + buffer.set_text("a5"); + assert_eq!(*current_value.borrow(), Some(1)); + assert!(entry.has_parse_error()); + } + + #[test] + fn it_update_on_empty_strings() { + gtk_init(); + let (current_value, entry) = setup_u32_entry(); + let buffer = entry.widget.buffer(); + + buffer.set_text("1"); + assert_eq!(*current_value.borrow(), Some(1)); + buffer.set_text(""); + assert_eq!(*current_value.borrow(), None); + + buffer.set_text("1"); + assert_eq!(*current_value.borrow(), Some(1)); + buffer.set_text("1a"); + assert_eq!(*current_value.borrow(), Some(1)); + assert!(entry.has_parse_error()); + + buffer.set_text(""); + assert_eq!(*current_value.borrow(), None); + assert!(!entry.has_parse_error()); + } } diff --git a/fitnesstrax/app/src/components/weight.rs b/fitnesstrax/app/src/components/weight.rs index 059e903..51b5012 100644 --- a/fitnesstrax/app/src/components/weight.rs +++ b/fitnesstrax/app/src/components/weight.rs @@ -47,21 +47,13 @@ pub fn weight_editor( on_update: OnUpdate, ) -> TextEntry> where - OnUpdate: Fn(si::Kilogram) + 'static, + OnUpdate: Fn(Option>) + 'static, { TextEntry::new( "0 kg", weight, |val: &si::Kilogram| val.to_string(), - move |v: &str| { - let new_weight = v.parse::().map(|w| w * si::KG).map_err(|_| ParseError); - match new_weight { - Ok(w) => { - on_update(w); - Ok(w) - } - Err(err) => Err(err), - } - }, + move |v: &str| v.parse::().map(|w| w * si::KG).map_err(|_| ParseError), + on_update, ) }