From a25b76d230c0fb82bb677b936139f0127ec1f909 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sun, 31 Dec 2023 11:18:15 -0500 Subject: [PATCH] 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::() + } +}