From bc31522c9520f0af32652960bd13b0241830a74e Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 30 Jan 2024 09:44:17 -0500 Subject: [PATCH] Add the on_update callback to TextEntry, and test the component --- fitnesstrax/app/src/components/day.rs | 10 +- fitnesstrax/app/src/components/steps.rs | 11 +- fitnesstrax/app/src/components/text_entry.rs | 116 +++++++++++++++---- fitnesstrax/app/src/components/weight.rs | 16 +-- 4 files changed, 106 insertions(+), 47 deletions(-) diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index 4e92180..970bf8b 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -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 3860ca9..391b43a 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 53702cb..a90ccbb 100644 --- a/fitnesstrax/app/src/components/text_entry.rs +++ b/fitnesstrax/app/src/components/text_entry.rs @@ -18,16 +18,16 @@ use crate::types::ParseError; use gtk::prelude::*; use std::{cell::RefCell, rc::Rc}; -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 { @@ -42,10 +42,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 { @@ -55,8 +62,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({ @@ -71,12 +78,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(_) => { @@ -85,25 +94,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 2afc259..1558f74 100644 --- a/fitnesstrax/app/src/components/weight.rs +++ b/fitnesstrax/app/src/components/weight.rs @@ -49,21 +49,13 @@ pub fn weight_editor( on_update: OnUpdate, ) -> TextEntry where - OnUpdate: Fn(WeightFormatter) + 'static, + OnUpdate: Fn(Option) + 'static, { TextEntry::new( "0 kg", weight, - |val: &WeightFormatter| val.format(FormatOption::Abbreviated), - move |v: &str| { - let new_weight = WeightFormatter::parse(v); - match new_weight { - Ok(w) => { - on_update(w); - Ok(w) - } - Err(err) => Err(err), - } - }, + |val| val.format(FormatOption::Abbreviated), + WeightFormatter::parse, + on_update, ) }