From 2e2ff6b47ebba9eb584965a9d25ab6c6f887846a Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 15 Jan 2024 13:20:23 -0500 Subject: [PATCH 1/7] Create a Singleton component and use it to simplify the weight view --- Cargo.lock | 2 +- fitnesstrax/app/src/components/mod.rs | 3 + fitnesstrax/app/src/components/singleton.rs | 68 ++++++++++++++++++++ fitnesstrax/app/src/components/text_entry.rs | 8 +-- fitnesstrax/app/src/components/weight.rs | 13 +++- 5 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 fitnesstrax/app/src/components/singleton.rs diff --git a/Cargo.lock b/Cargo.lock index a252307..3340ab5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -699,7 +699,7 @@ dependencies = [ [[package]] name = "dashboard" -version = "0.1.1" +version = "0.1.2" dependencies = [ "cairo-rs", "chrono", diff --git a/fitnesstrax/app/src/components/mod.rs b/fitnesstrax/app/src/components/mod.rs index 19ee548..9f1cf16 100644 --- a/fitnesstrax/app/src/components/mod.rs +++ b/fitnesstrax/app/src/components/mod.rs @@ -20,6 +20,9 @@ pub use day::{DayDetail, DaySummary}; mod edit_view; pub use edit_view::EditView; +mod singleton; +pub use singleton::Singleton; + mod text_entry; pub use text_entry::{ParseError, TextEntry}; diff --git a/fitnesstrax/app/src/components/singleton.rs b/fitnesstrax/app/src/components/singleton.rs new file mode 100644 index 0000000..5dec01f --- /dev/null +++ b/fitnesstrax/app/src/components/singleton.rs @@ -0,0 +1,68 @@ +/* +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 . +*/ + +//! A Widget container for a single components +use glib::Object; +use gtk::{prelude::*, subclass::prelude::*}; +use std::cell::RefCell; + +pub struct SingletonPrivate { + widget: RefCell, +} + +impl Default for SingletonPrivate { + fn default() -> Self { + Self { + widget: RefCell::new(gtk::Label::new(None).upcast()), + } + } +} + +#[glib::object_subclass] +impl ObjectSubclass for SingletonPrivate { + const NAME: &'static str = "Singleton"; + type Type = Singleton; + type ParentType = gtk::Box; +} + +impl ObjectImpl for SingletonPrivate {} +impl WidgetImpl for SingletonPrivate {} +impl BoxImpl for SingletonPrivate {} + +glib::wrapper! { + /// The Singleton component contains exactly one child widget. The swap function makes it easy + /// to handle the job of swapping that child out for a different one. + pub struct Singleton(ObjectSubclass) @extends gtk::Box, gtk::Widget; +} + +impl Singleton { + /// Construct a Singleton object. The Singleton defaults to an invisible child. + pub fn new() -> Self { + let s: Self = Object::builder().build(); + + s.append(&*s.imp().widget.borrow()); + + s + } + + /// Replace the currently visible child widget with a new one. The old widget will be + /// discarded. + pub fn swap(&self, new_widget: >k::Widget) { + self.remove(&*self.imp().widget.borrow()); + self.append(new_widget); + *self.imp().widget.borrow_mut() = new_widget.clone(); + } +} diff --git a/fitnesstrax/app/src/components/text_entry.rs b/fitnesstrax/app/src/components/text_entry.rs index 2220ec9..750ca2e 100644 --- a/fitnesstrax/app/src/components/text_entry.rs +++ b/fitnesstrax/app/src/components/text_entry.rs @@ -24,12 +24,12 @@ pub struct TextEntry { value: Rc>>, widget: gtk::Entry, renderer: Rc String>>, - validator: Rc Result>>, + parser: Rc Result>>, } // I do not understand why the data should be 'static. impl TextEntry { - pub fn new(placeholder: &str, value: Option, renderer: R, validator: V) -> Self + pub fn new(placeholder: &str, value: Option, renderer: R, parser: V) -> Self where R: Fn(&T) -> String + 'static, V: Fn(&str) -> Result + 'static, @@ -44,7 +44,7 @@ impl TextEntry { value: Rc::new(RefCell::new(value)), widget, renderer: Rc::new(Box::new(renderer)), - validator: Rc::new(Box::new(validator)), + parser: Rc::new(Box::new(parser)), }; s.widget.buffer().connect_text_notify({ @@ -61,7 +61,7 @@ impl TextEntry { self.widget.remove_css_class("error"); return; } - match (self.validator)(buffer.text().as_str()) { + match (self.parser)(buffer.text().as_str()) { Ok(v) => { *self.value.borrow_mut() = Some(v); self.widget.remove_css_class("error"); diff --git a/fitnesstrax/app/src/components/weight.rs b/fitnesstrax/app/src/components/weight.rs index f32d11e..3108bc7 100644 --- a/fitnesstrax/app/src/components/weight.rs +++ b/fitnesstrax/app/src/components/weight.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::{EditView, ParseError, TextEntry}; +use crate::components::{EditView, ParseError, Singleton, TextEntry}; use chrono::{Local, NaiveDate}; use dimensioned::si; use ft_core::Weight; @@ -27,6 +27,7 @@ pub struct WeightViewPrivate { record: RefCell>, widget: RefCell>>>, + container: Singleton, on_edit_finished: RefCell)>>, } @@ -37,6 +38,7 @@ impl Default for WeightViewPrivate { date: RefCell::new(Local::now().date_naive()), record: RefCell::new(None), widget: RefCell::new(EditView::Unconfigured), + container: Singleton::new(), on_edit_finished: RefCell::new(Box::new(|_| {})), } } @@ -68,6 +70,8 @@ impl WeightView { { let s: Self = Object::builder().build(); + s.append(&s.imp().container); + *s.imp().on_edit_finished.borrow_mut() = Box::new(on_edit_finished); *s.imp().date.borrow_mut() = date; @@ -126,6 +130,12 @@ impl WeightView { } fn swap(&self, new_view: EditView>>) { + match new_view { + EditView::Unconfigured => {} + EditView::View(view) => self.imp().container.swap(&view.upcast()), + EditView::Edit(editor) => self.imp().container.swap(&editor.widget()), + } + /* let mut widget = self.imp().widget.borrow_mut(); match *widget { EditView::Unconfigured => {} @@ -139,6 +149,7 @@ impl WeightView { EditView::Edit(ref editor) => self.append(&editor.widget()), } *widget = new_view; + */ } pub fn blur(&self) { -- 2.44.1 From 1e6555ef6187401439d0061f7ff3ec2da85a0d01 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 15 Jan 2024 15:53:01 -0500 Subject: [PATCH 2/7] Create a day detail view DayDetail, the component, I used to use as a view. Now I'm swapping things out so that DayDetailView handles the view itself. The DayDetail component will still show the details of the day, but I'll create a DayEditComponent which is dedicated to showing the edit interface for everything in a day. The swapping will now happen in DayDetailView, not in DayDetail or an even deeper component. --- fitnesstrax/app/src/app_window.rs | 18 +-- fitnesstrax/app/src/components/day.rs | 89 ++++++++++---- fitnesstrax/app/src/components/mod.rs | 6 +- fitnesstrax/app/src/components/singleton.rs | 19 +-- fitnesstrax/app/src/components/weight.rs | 115 ++++++++++++++----- fitnesstrax/app/src/views/day_detail_view.rs | 82 +++++++++++++ fitnesstrax/app/src/views/mod.rs | 3 + 7 files changed, 253 insertions(+), 79 deletions(-) create mode 100644 fitnesstrax/app/src/views/day_detail_view.rs diff --git a/fitnesstrax/app/src/app_window.rs b/fitnesstrax/app/src/app_window.rs index 69d6ab1..5fc3213 100644 --- a/fitnesstrax/app/src/app_window.rs +++ b/fitnesstrax/app/src/app_window.rs @@ -1,5 +1,5 @@ /* -Copyright 2023, Savanni D'Gerinel +Copyright 2023-2024, Savanni D'Gerinel This file is part of FitnessTrax. @@ -16,8 +16,7 @@ You should have received a copy of the GNU General Public License along with Fit use crate::{ app::App, - components::DayDetail, - views::{HistoricalView, PlaceholderView, View, WelcomeView}, + views::{DayDetailView, HistoricalView, PlaceholderView, View, WelcomeView}, }; use adw::prelude::*; use chrono::{Duration, Local}; @@ -138,18 +137,7 @@ impl AppWindow { Rc::new(move |date, records| { let layout = gtk::Box::new(gtk::Orientation::Vertical, 0); layout.append(&adw::HeaderBar::new()); - layout.append(&DayDetail::new( - date, - records, - { - let s = s.clone(); - move |record| s.on_put_record(record) - }, - { - let s = s.clone(); - move |record| s.on_update_record(record) - }, - )); + layout.append(&DayDetailView::new(date, records, s.app.clone())); let page = &adw::NavigationPage::builder() .title(date.format("%Y-%m-%d").to_string()) .child(&layout) diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index 8735086..25a4b5b 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -16,9 +16,8 @@ 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::{TimeDistanceView, WeightView}; +use crate::components::{TimeDistanceView, Weight}; use emseries::Record; -use ft_core::{RecordType, TraxRecord, Weight}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::cell::RefCell; @@ -67,7 +66,7 @@ impl DaySummary { s } - pub fn set_data(&self, date: chrono::NaiveDate, records: Vec>) { + pub fn set_data(&self, date: chrono::NaiveDate, records: Vec>) { self.imp() .date .set_text(&date.format("%Y-%m-%d").to_string()); @@ -77,7 +76,7 @@ impl DaySummary { } if let Some(Record { - data: TraxRecord::Weight(weight_record), + data: ft_core::TraxRecord::Weight(weight_record), .. }) = records.iter().filter(|f| f.data.is_weight()).next() { @@ -101,6 +100,33 @@ impl DaySummary { } } +#[derive(Default)] +struct CommandRowPrivate; + +#[glib::object_subclass] +impl ObjectSubclass for CommandRowPrivate { + const NAME: &'static str = "DayDetailCommandRow"; + type Type = CommandRow; + type ParentType = gtk::Box; +} + +impl ObjectImpl for CommandRowPrivate {} +impl WidgetImpl for CommandRowPrivate {} +impl BoxImpl for CommandRowPrivate {} + +glib::wrapper! { + struct CommandRow(ObjectSubclass) @extends gtk::Box, gtk::Widget; +} + +impl CommandRow { + fn new() -> Self { + let s: Self = Object::builder().build(); + s.set_halign(gtk::Align::End); + s.append(>k::Button::builder().label("Edit").build()); + s + } +} + pub struct DayDetailPrivate { date: gtk::Label, weight: RefCell>, @@ -135,17 +161,21 @@ glib::wrapper! { impl DayDetail { pub fn new( date: chrono::NaiveDate, - records: Vec>, + records: Vec>, on_put_record: PutRecordFn, on_update_record: UpdateRecordFn, ) -> Self where - PutRecordFn: Fn(TraxRecord) + 'static, - UpdateRecordFn: Fn(Record) + 'static, + PutRecordFn: Fn(ft_core::TraxRecord) + 'static, + UpdateRecordFn: Fn(Record) + 'static, { let s: Self = Object::builder().build(); s.set_orientation(gtk::Orientation::Vertical); + s.set_hexpand(true); + s.append(&CommandRow::new()); + + /* let click_controller = gtk::GestureClick::new(); click_controller.connect_released({ let s = s.clone(); @@ -158,52 +188,65 @@ impl DayDetail { } }); s.add_controller(click_controller); + */ let weight_record = records.iter().find_map(|record| match record { Record { id, - data: TraxRecord::Weight(record), + data: ft_core::TraxRecord::Weight(record), } => Some((id.clone(), record.clone())), _ => None, }); let weight_view = match weight_record { - Some((id, data)) => WeightView::new(date.clone(), Some(data.clone()), move |weight| { + Some((id, data)) => Weight::new(Some(data.clone()), move |weight| { on_update_record(Record { id: id.clone(), - data: TraxRecord::Weight(Weight { date, weight }), + data: ft_core::TraxRecord::Weight(ft_core::Weight { date, weight }), }) }), - None => WeightView::new(date.clone(), None, move |weight| { - on_put_record(TraxRecord::Weight(Weight { date, weight })); + None => Weight::new(None, move |weight| { + on_put_record(ft_core::TraxRecord::Weight(ft_core::Weight { + date, + weight, + })); }), }; - s.append(&weight_view); + s.append(&weight_view.widget()); records.into_iter().for_each(|record| { let record_view = match record { Record { - data: TraxRecord::BikeRide(record), + data: ft_core::TraxRecord::BikeRide(record), .. } => Some( - TimeDistanceView::new(RecordType::BikeRide, record).upcast::(), + TimeDistanceView::new(ft_core::RecordType::BikeRide, record) + .upcast::(), ), Record { - data: TraxRecord::Row(record), + data: ft_core::TraxRecord::Row(record), .. - } => Some(TimeDistanceView::new(RecordType::Row, record).upcast::()), + } => Some( + TimeDistanceView::new(ft_core::RecordType::Row, record).upcast::(), + ), Record { - data: TraxRecord::Run(record), + data: ft_core::TraxRecord::Run(record), .. - } => Some(TimeDistanceView::new(RecordType::Row, record).upcast::()), + } => Some( + TimeDistanceView::new(ft_core::RecordType::Row, record).upcast::(), + ), Record { - data: TraxRecord::Swim(record), + data: ft_core::TraxRecord::Swim(record), .. - } => Some(TimeDistanceView::new(RecordType::Row, record).upcast::()), + } => Some( + TimeDistanceView::new(ft_core::RecordType::Row, record).upcast::(), + ), Record { - data: TraxRecord::Walk(record), + data: ft_core::TraxRecord::Walk(record), .. - } => Some(TimeDistanceView::new(RecordType::Row, record).upcast::()), + } => Some( + TimeDistanceView::new(ft_core::RecordType::Row, record).upcast::(), + ), _ => None, }; diff --git a/fitnesstrax/app/src/components/mod.rs b/fitnesstrax/app/src/components/mod.rs index 9f1cf16..fbc4e24 100644 --- a/fitnesstrax/app/src/components/mod.rs +++ b/fitnesstrax/app/src/components/mod.rs @@ -1,5 +1,5 @@ /* -Copyright 2023, Savanni D'Gerinel +Copyright 2023-2024, Savanni D'Gerinel This file is part of FitnessTrax. @@ -21,7 +21,7 @@ mod edit_view; pub use edit_view::EditView; mod singleton; -pub use singleton::Singleton; +pub use singleton::{Singleton, SingletonImpl}; mod text_entry; pub use text_entry::{ParseError, TextEntry}; @@ -30,7 +30,7 @@ mod time_distance; pub use time_distance::TimeDistanceView; mod weight; -pub use weight::WeightView; +pub use weight::Weight; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; diff --git a/fitnesstrax/app/src/components/singleton.rs b/fitnesstrax/app/src/components/singleton.rs index 5dec01f..340c090 100644 --- a/fitnesstrax/app/src/components/singleton.rs +++ b/fitnesstrax/app/src/components/singleton.rs @@ -48,21 +48,24 @@ glib::wrapper! { pub struct Singleton(ObjectSubclass) @extends gtk::Box, gtk::Widget; } -impl Singleton { - /// Construct a Singleton object. The Singleton defaults to an invisible child. - pub fn new() -> Self { +impl Default for Singleton { + fn default() -> Self { let s: Self = Object::builder().build(); s.append(&*s.imp().widget.borrow()); s } +} - /// Replace the currently visible child widget with a new one. The old widget will be - /// discarded. - pub fn swap(&self, new_widget: >k::Widget) { +impl Singleton { + pub fn swap(&self, new_widget: &impl IsA) { + let new_widget = new_widget.clone().upcast(); self.remove(&*self.imp().widget.borrow()); - self.append(new_widget); - *self.imp().widget.borrow_mut() = new_widget.clone(); + self.append(&new_widget); + *self.imp().widget.borrow_mut() = new_widget; } } + +pub trait SingletonImpl: WidgetImpl + BoxImpl {} +unsafe impl IsSubclassable for Singleton {} diff --git a/fitnesstrax/app/src/components/weight.rs b/fitnesstrax/app/src/components/weight.rs index 3108bc7..25c3f56 100644 --- a/fitnesstrax/app/src/components/weight.rs +++ b/fitnesstrax/app/src/components/weight.rs @@ -17,46 +17,50 @@ You should have received a copy of the GNU General Public License along with Fit use crate::components::{EditView, ParseError, Singleton, TextEntry}; use chrono::{Local, NaiveDate}; use dimensioned::si; -use ft_core::Weight; -use glib::Object; +use glib::{object::ObjectRef, Object}; use gtk::{prelude::*, subclass::prelude::*}; -use std::cell::RefCell; +use std::{borrow::Borrow, cell::RefCell}; +#[derive(Default)] pub struct WeightViewPrivate { + /* date: RefCell, record: RefCell>, widget: RefCell>>>, - container: Singleton, 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), - container: Singleton::new(), + container: Singleton::default(), 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; + type ParentType = gtk::Label; } impl ObjectImpl for WeightViewPrivate {} impl WidgetImpl for WeightViewPrivate {} -impl BoxImpl for WeightViewPrivate {} +impl LabelImpl for WeightViewPrivate {} glib::wrapper! { - pub struct WeightView(ObjectSubclass) @extends gtk::Box, gtk::Widget; + pub struct WeightView(ObjectSubclass) @extends gtk::Label, gtk::Widget; } impl WeightView { @@ -69,18 +73,33 @@ impl WeightView { OnEditFinished: Fn(si::Kilogram) + 'static, { let s: Self = Object::builder().build(); + s.set_css_classes(&["card", "weight-view"]); + s.set_can_focus(true); s.append(&s.imp().container); + match weight { + Some(weight) => { + s.remove_css_class("dim_label"); + s.set_label(&format!("{:?}", weight)); + } + None => { + s.add_css_class("dim_label"); + s.set_label("No weight recorded"); + } + } + /* *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"]) @@ -109,7 +128,7 @@ impl WeightView { } } - self.swap(EditView::View(view)); + // self.swap(EditView::View(view)); } fn edit(&self) { @@ -125,33 +144,37 @@ impl WeightView { None => edit.set_value(None), } - self.swap(EditView::Edit(edit.clone())); + // self.swap(EditView::Edit(edit.clone())); edit.grab_focus(); } + */ - fn swap(&self, new_view: EditView>>) { - match new_view { - EditView::Unconfigured => {} - EditView::View(view) => self.imp().container.swap(&view.upcast()), - EditView::Edit(editor) => self.imp().container.swap(&editor.widget()), - } - /* - 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()), - } + /* + fn swap(&self, new_view: EditView>>) { + match new_view { + EditView::Unconfigured => {} + EditView::View(view) => self.imp().container.swap(&view.upcast()), + EditView::Edit(editor) => self.imp().container.swap(&editor.widget()), + } + /* + 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()), + match new_view { + EditView::Unconfigured => {} + EditView::View(ref view) => self.append(view), + EditView::Edit(ref editor) => self.append(&editor.widget()), + } + *widget = new_view; + */ } - *widget = new_view; - */ - } + */ + /* pub fn blur(&self) { match *self.imp().widget.borrow() { EditView::Unconfigured => {} @@ -194,4 +217,36 @@ impl WeightView { self.view(); } + */ +} +*/ + +pub struct Weight { + label: gtk::Label, +} + +impl Weight { + pub fn new( + weight: Option, + on_edit_finished: OnEditFinished, + ) -> Self + where + OnEditFinished: Fn(si::Kilogram) + 'static, + { + let label = gtk::Label::builder() + .css_classes(["card", "weight-view"]) + .can_focus(true) + .build(); + + match weight { + Some(w) => label.set_text(&format!("{:?}", w.weight)), + None => label.set_text("No weight recorded"), + } + + Self { label } + } + + pub fn widget(&self) -> gtk::Widget { + self.label.clone().upcast() + } } diff --git a/fitnesstrax/app/src/views/day_detail_view.rs b/fitnesstrax/app/src/views/day_detail_view.rs new file mode 100644 index 0000000..4c68304 --- /dev/null +++ b/fitnesstrax/app/src/views/day_detail_view.rs @@ -0,0 +1,82 @@ +/* +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::{DayDetail, Singleton, SingletonImpl}, +}; +use emseries::Record; +use ft_core::TraxRecord; +use glib::Object; +use gtk::{prelude::*, subclass::prelude::*}; + +#[derive(Default)] +pub struct DayDetailViewPrivate { + container: Singleton, +} + +#[glib::object_subclass] +impl ObjectSubclass for DayDetailViewPrivate { + const NAME: &'static str = "DayDetailView"; + type Type = DayDetailView; + type ParentType = gtk::Box; +} + +impl ObjectImpl for DayDetailViewPrivate {} +impl WidgetImpl for DayDetailViewPrivate {} +impl BoxImpl for DayDetailViewPrivate {} +impl SingletonImpl for DayDetailViewPrivate {} + +glib::wrapper! { + pub struct DayDetailView(ObjectSubclass) @extends gtk::Box, gtk::Widget; +} + +impl DayDetailView { + pub fn new(date: chrono::NaiveDate, records: Vec>, app: App) -> Self { + let s: Self = Object::builder().build(); + + s.append(&s.imp().container); + + s.imp().container.swap(&DayDetail::new( + date, + records, + { + let app = app.clone(); + move |record| { + let app = app.clone(); + glib::spawn_future_local({ + async move { + app.put_record(record).await; + } + }); + } + }, + { + let app = app.clone(); + move |record| { + let app = app.clone(); + glib::spawn_future_local({ + async move { + app.update_record(record).await; + } + }); + } + }, + )); + + s + } +} diff --git a/fitnesstrax/app/src/views/mod.rs b/fitnesstrax/app/src/views/mod.rs index e014581..9957823 100644 --- a/fitnesstrax/app/src/views/mod.rs +++ b/fitnesstrax/app/src/views/mod.rs @@ -16,6 +16,9 @@ You should have received a copy of the GNU General Public License along with Fit use gtk::prelude::*; +mod day_detail_view; +pub use day_detail_view::DayDetailView; + mod historical_view; pub use historical_view::HistoricalView; -- 2.44.1 From 104760c754d200cef656d7ca2c5e00a957e08248 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 15 Jan 2024 23:27:55 -0500 Subject: [PATCH 3/7] Be able to switch into edit mode --- fitnesstrax/app/src/components/day.rs | 113 +++++++++++++++---- fitnesstrax/app/src/components/mod.rs | 2 +- fitnesstrax/app/src/components/weight.rs | 33 ++++-- fitnesstrax/app/src/views/day_detail_view.rs | 103 ++++++++++++----- 4 files changed, 196 insertions(+), 55 deletions(-) diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index 25a4b5b..066b0f1 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -1,5 +1,5 @@ /* -Copyright 2023, Savanni D'Gerinel +Copyright 2023-2024, Savanni D'Gerinel This file is part of FitnessTrax. @@ -22,6 +22,8 @@ use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::cell::RefCell; +use super::weight::WeightEdit; + pub struct DaySummaryPrivate { date: gtk::Label, weight: RefCell>, @@ -119,10 +121,17 @@ glib::wrapper! { } impl CommandRow { - fn new() -> Self { + fn new(on_edit: OnEdit) -> Self + where + OnEdit: Fn() + 'static, + { let s: Self = Object::builder().build(); s.set_halign(gtk::Align::End); - s.append(>k::Button::builder().label("Edit").build()); + + let edit_button = gtk::Button::builder().label("Edit").build(); + edit_button.connect_clicked(move |_| on_edit()); + s.append(&edit_button); + s } } @@ -159,21 +168,19 @@ glib::wrapper! { } impl DayDetail { - pub fn new( + pub fn new( date: chrono::NaiveDate, records: Vec>, - on_put_record: PutRecordFn, - on_update_record: UpdateRecordFn, + on_edit: OnEdit, ) -> Self where - PutRecordFn: Fn(ft_core::TraxRecord) + 'static, - UpdateRecordFn: Fn(Record) + 'static, + OnEdit: Fn() + 'static, { let s: Self = Object::builder().build(); s.set_orientation(gtk::Orientation::Vertical); s.set_hexpand(true); - s.append(&CommandRow::new()); + s.append(&CommandRow::new(on_edit)); /* let click_controller = gtk::GestureClick::new(); @@ -199,18 +206,8 @@ impl DayDetail { }); let weight_view = match weight_record { - Some((id, data)) => Weight::new(Some(data.clone()), move |weight| { - on_update_record(Record { - id: id.clone(), - data: ft_core::TraxRecord::Weight(ft_core::Weight { date, weight }), - }) - }), - None => Weight::new(None, move |weight| { - on_put_record(ft_core::TraxRecord::Weight(ft_core::Weight { - date, - weight, - })); - }), + Some((id, data)) => Weight::new(Some(data.clone())), + None => Weight::new(None), }; s.append(&weight_view.widget()); @@ -261,3 +258,77 @@ impl DayDetail { s } } + +pub struct DayEditPrivate { + date: gtk::Label, + weight: WeightEdit, +} + +impl Default for DayEditPrivate { + fn default() -> Self { + Self { + date: gtk::Label::new(None), + weight: WeightEdit::new(None), + } + } +} + +#[glib::object_subclass] +impl ObjectSubclass for DayEditPrivate { + const NAME: &'static str = "DayEdit"; + type Type = DayEdit; + type ParentType = gtk::Box; +} + +impl ObjectImpl for DayEditPrivate {} +impl WidgetImpl for DayEditPrivate {} +impl BoxImpl for DayEditPrivate {} + +glib::wrapper! { + pub struct DayEdit(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; +} + +impl DayEdit { + pub fn new( + date: chrono::NaiveDate, + records: Vec>, + on_put_record: PutRecordFn, + on_update_record: UpdateRecordFn, + ) -> Self + where + PutRecordFn: Fn(ft_core::TraxRecord) + 'static, + UpdateRecordFn: Fn(Record) + 'static, + { + let s: Self = Object::builder().build(); + + /*, move |weight| { + on_update_record(Record { + id: id.clone(), + data: ft_core::TraxRecord::Weight(ft_core::Weight { date, weight }), + }) + }*/ + + /*, move |weight| { + on_put_record(ft_core::TraxRecord::Weight(ft_core::Weight { + date, + weight, + })); + }*/ + + let weight_record = records.iter().find_map(|record| match record { + Record { + id, + data: ft_core::TraxRecord::Weight(record), + } => Some((id.clone(), record.clone())), + _ => None, + }); + + let weight_view = match weight_record { + Some((_id, data)) => WeightEdit::new(Some(data.clone())), + None => WeightEdit::new(None), + }; + s.append(&weight_view.widget()); + + s + } +} diff --git a/fitnesstrax/app/src/components/mod.rs b/fitnesstrax/app/src/components/mod.rs index fbc4e24..b5adeff 100644 --- a/fitnesstrax/app/src/components/mod.rs +++ b/fitnesstrax/app/src/components/mod.rs @@ -15,7 +15,7 @@ You should have received a copy of the GNU General Public License along with Fit */ mod day; -pub use day::{DayDetail, DaySummary}; +pub use day::{DayDetail, DayEdit, DaySummary}; mod edit_view; pub use edit_view::EditView; diff --git a/fitnesstrax/app/src/components/weight.rs b/fitnesstrax/app/src/components/weight.rs index 25c3f56..69db27e 100644 --- a/fitnesstrax/app/src/components/weight.rs +++ b/fitnesstrax/app/src/components/weight.rs @@ -226,13 +226,7 @@ pub struct Weight { } impl Weight { - pub fn new( - weight: Option, - on_edit_finished: OnEditFinished, - ) -> Self - where - OnEditFinished: Fn(si::Kilogram) + 'static, - { + pub fn new(weight: Option) -> Self { let label = gtk::Label::builder() .css_classes(["card", "weight-view"]) .can_focus(true) @@ -250,3 +244,28 @@ impl Weight { self.label.clone().upcast() } } + +pub struct WeightEdit { + entry: TextEntry>, +} + +impl WeightEdit { + pub fn new(weight: Option) -> Self { + Self { + entry: TextEntry::new( + "0 kg", + weight.map(|w| w.weight), + |val: &si::Kilogram| val.to_string(), + |v: &str| v.parse::().map(|w| w * si::KG).map_err(|_| ParseError), + ), + } + } + + pub fn value(&self) -> Option> { + self.entry.value() + } + + pub fn widget(&self) -> gtk::Widget { + self.entry.widget() + } +} diff --git a/fitnesstrax/app/src/views/day_detail_view.rs b/fitnesstrax/app/src/views/day_detail_view.rs index 4c68304..65bc114 100644 --- a/fitnesstrax/app/src/views/day_detail_view.rs +++ b/fitnesstrax/app/src/views/day_detail_view.rs @@ -16,16 +16,20 @@ You should have received a copy of the GNU General Public License along with Fit use crate::{ app::App, - components::{DayDetail, Singleton, SingletonImpl}, + components::{DayDetail, DayEdit, Singleton, SingletonImpl}, }; use emseries::Record; use ft_core::TraxRecord; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; +use std::cell::RefCell; #[derive(Default)] pub struct DayDetailViewPrivate { + app: RefCell>, container: Singleton, + date: RefCell, + records: RefCell>>, } #[glib::object_subclass] @@ -48,35 +52,82 @@ impl DayDetailView { pub fn new(date: chrono::NaiveDate, records: Vec>, app: App) -> Self { let s: Self = Object::builder().build(); + *s.imp().date.borrow_mut() = date; + *s.imp().records.borrow_mut() = records; + s.append(&s.imp().container); - s.imp().container.swap(&DayDetail::new( - date, - records, - { - let app = app.clone(); - move |record| { - let app = app.clone(); - glib::spawn_future_local({ - async move { - app.put_record(record).await; - } - }); + /* + s.imp() + .container + .swap(&DayDetail::new(date, records.clone(), { + let s = s.clone(); + let records = records.clone(); + move || { + s.imp().container.swap(&DayEdit::new( + date, + records, + s.on_put_record(), + // s.on_update_record(), + |_| {}, + )) } - }, - { - let app = app.clone(); - move |record| { - let app = app.clone(); - glib::spawn_future_local({ - async move { - app.update_record(record).await; - } - }); - } - }, - )); + })); + */ + + s.view(); s } + + fn view(&self) { + self.imp().container.swap(&DayDetail::new( + self.imp().date.borrow().clone(), + self.imp().records.borrow().clone(), + { + let s = self.clone(); + move || s.edit() + }, + )); + } + + fn edit(&self) { + self.imp().container.swap(&DayEdit::new( + self.imp().date.borrow().clone(), + self.imp().records.borrow().clone(), + |_| {}, + |_| {}, + )); + } + + fn on_put_record(&self) -> Box { + let app = self.imp().app.clone(); + Box::new(move |record| { + let app = app.clone(); + glib::spawn_future_local({ + async move { + match &*app.borrow() { + Some(app) => { + let _ = app.put_record(record).await; + } + None => {} + } + } + }); + }) + } + + /* + fn on_update_record(&self, record: TraxRecord) -> dyn Fn(ft_core::TraxRecord) { + let app = self.imp().app.clone(); + move |record| { + let app = app.clone(); + glib::spawn_future_local({ + async move { + app.update_record(record).await; + } + }); + } + } + */ } -- 2.44.1 From b00acc64a37eac07bdfac35a77d3f61fac93b225 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Wed, 17 Jan 2024 22:13:55 -0500 Subject: [PATCH 4/7] Set up the ActionGroup component --- .../app/src/components/action_group.rs | 135 ++++++++++++ fitnesstrax/app/src/components/day.rs | 66 +++--- fitnesstrax/app/src/components/mod.rs | 3 + fitnesstrax/app/src/components/weight.rs | 199 +----------------- fitnesstrax/app/src/views/day_detail_view.rs | 23 +- 5 files changed, 180 insertions(+), 246 deletions(-) create mode 100644 fitnesstrax/app/src/components/action_group.rs diff --git a/fitnesstrax/app/src/components/action_group.rs b/fitnesstrax/app/src/components/action_group.rs new file mode 100644 index 0000000..4197edd --- /dev/null +++ b/fitnesstrax/app/src/components/action_group.rs @@ -0,0 +1,135 @@ +/* +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 glib::Object; +use gtk::{prelude::*, subclass::prelude::*}; + +#[derive(Default)] +pub struct ActionGroupPrivate; + +#[glib::object_subclass] +impl ObjectSubclass for ActionGroupPrivate { + const NAME: &'static str = "ActionGroup"; + type Type = ActionGroup; + type ParentType = gtk::Box; +} + +impl ObjectImpl for ActionGroupPrivate {} +impl WidgetImpl for ActionGroupPrivate {} +impl BoxImpl for ActionGroupPrivate {} + +glib::wrapper! { + pub struct ActionGroup(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; +} + +impl ActionGroup { + fn new(builder: ActionGroupBuilder) -> Self { + let s: Self = Object::builder().build(); + s.set_orientation(builder.orientation); + + let primary_button = builder.primary_action.button(); + let secondary_button = builder.secondary_action.map(|action| action.button()); + let tertiary_button = builder.tertiary_action.map(|action| action.button()); + + if let Some(button) = tertiary_button { + s.append(&button); + } + + s.set_halign(gtk::Align::End); + if let Some(button) = secondary_button { + s.append(&button); + } + s.append(&primary_button); + + s + } + + pub fn builder() -> ActionGroupBuilder { + ActionGroupBuilder { + orientation: gtk::Orientation::Horizontal, + primary_action: Action { + label: "Ok".to_owned(), + action: Box::new(|| {}), + }, + secondary_action: None, + tertiary_action: None, + } + } +} + +struct Action { + label: String, + action: Box, +} + +impl Action { + fn button(self) -> gtk::Button { + let button = gtk::Button::builder().label(self.label).build(); + button.connect_clicked(move |_| (self.action)()); + button + } +} + +pub struct ActionGroupBuilder { + orientation: gtk::Orientation, + primary_action: Action, + secondary_action: Option, + tertiary_action: Option, +} + +impl ActionGroupBuilder { + pub fn orientation(mut self, orientation: gtk::Orientation) -> Self { + self.orientation = orientation; + self + } + + pub fn primary_action(mut self, label: &str, action: A) -> Self + where + A: Fn() + 'static, + { + self.primary_action = Action { + label: label.to_owned(), + action: Box::new(action), + }; + self + } + + pub fn secondary_action(mut self, label: &str, action: A) -> Self + where + A: Fn() + 'static, + { + self.secondary_action = Some(Action { + label: label.to_owned(), + action: Box::new(action), + }); + self + } + + pub fn tertiary_action(mut self, label: &str, action: A) -> Self + where + A: Fn() + 'static, + { + self.tertiary_action = Some(Action { + label: label.to_owned(), + action: Box::new(action), + }); + self + } + + pub fn build(self) -> ActionGroup { + ActionGroup::new(self) + } +} diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index 066b0f1..c72a270 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -16,11 +16,11 @@ 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::{TimeDistanceView, Weight}; +use crate::components::{ActionGroup, TimeDistanceView, Weight}; use emseries::Record; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; -use std::cell::RefCell; +use std::{cell::RefCell, rc::Rc}; use super::weight::WeightEdit; @@ -102,40 +102,6 @@ impl DaySummary { } } -#[derive(Default)] -struct CommandRowPrivate; - -#[glib::object_subclass] -impl ObjectSubclass for CommandRowPrivate { - const NAME: &'static str = "DayDetailCommandRow"; - type Type = CommandRow; - type ParentType = gtk::Box; -} - -impl ObjectImpl for CommandRowPrivate {} -impl WidgetImpl for CommandRowPrivate {} -impl BoxImpl for CommandRowPrivate {} - -glib::wrapper! { - struct CommandRow(ObjectSubclass) @extends gtk::Box, gtk::Widget; -} - -impl CommandRow { - fn new(on_edit: OnEdit) -> Self - where - OnEdit: Fn() + 'static, - { - let s: Self = Object::builder().build(); - s.set_halign(gtk::Align::End); - - let edit_button = gtk::Button::builder().label("Edit").build(); - edit_button.connect_clicked(move |_| on_edit()); - s.append(&edit_button); - - s - } -} - pub struct DayDetailPrivate { date: gtk::Label, weight: RefCell>, @@ -180,7 +146,11 @@ impl DayDetail { s.set_orientation(gtk::Orientation::Vertical); s.set_hexpand(true); - s.append(&CommandRow::new(on_edit)); + s.append( + &ActionGroup::builder() + .primary_action("Edit", Box::new(on_edit)) + .build(), + ); /* let click_controller = gtk::GestureClick::new(); @@ -261,14 +231,14 @@ impl DayDetail { pub struct DayEditPrivate { date: gtk::Label, - weight: WeightEdit, + weight: Rc, } impl Default for DayEditPrivate { fn default() -> Self { Self { date: gtk::Label::new(None), - weight: WeightEdit::new(None), + weight: Rc::new(WeightEdit::new(None)), } } } @@ -289,17 +259,21 @@ glib::wrapper! { } impl DayEdit { - pub fn new( + pub fn new( date: chrono::NaiveDate, records: Vec>, on_put_record: PutRecordFn, on_update_record: UpdateRecordFn, + on_cancel: CancelFn, ) -> Self where PutRecordFn: Fn(ft_core::TraxRecord) + 'static, UpdateRecordFn: Fn(Record) + 'static, + CancelFn: Fn() + 'static, { let s: Self = Object::builder().build(); + s.set_orientation(gtk::Orientation::Vertical); + s.set_hexpand(true); /*, move |weight| { on_update_record(Record { @@ -315,6 +289,18 @@ impl DayEdit { })); }*/ + s.append( + &ActionGroup::builder() + .primary_action("Save", { + let s = s.clone(); + move || { + println!("weight value: {:?}", s.imp().weight.value()); + } + }) + .secondary_action("Cancel", on_cancel) + .build(), + ); + let weight_record = records.iter().find_map(|record| match record { Record { id, diff --git a/fitnesstrax/app/src/components/mod.rs b/fitnesstrax/app/src/components/mod.rs index b5adeff..75ecdd2 100644 --- a/fitnesstrax/app/src/components/mod.rs +++ b/fitnesstrax/app/src/components/mod.rs @@ -14,6 +14,9 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see . */ +mod action_group; +pub use action_group::ActionGroup; + mod day; pub use day::{DayDetail, DayEdit, DaySummary}; diff --git a/fitnesstrax/app/src/components/weight.rs b/fitnesstrax/app/src/components/weight.rs index 69db27e..e2fb631 100644 --- a/fitnesstrax/app/src/components/weight.rs +++ b/fitnesstrax/app/src/components/weight.rs @@ -22,204 +22,7 @@ use gtk::{prelude::*, subclass::prelude::*}; use std::{borrow::Borrow, cell::RefCell}; #[derive(Default)] -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), - container: Singleton::default(), - 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::Label; -} - -impl ObjectImpl for WeightViewPrivate {} -impl WidgetImpl for WeightViewPrivate {} -impl LabelImpl for WeightViewPrivate {} - -glib::wrapper! { - pub struct WeightView(ObjectSubclass) @extends gtk::Label, 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.set_css_classes(&["card", "weight-view"]); - s.set_can_focus(true); - - s.append(&s.imp().container); - - match weight { - Some(weight) => { - s.remove_css_class("dim_label"); - s.set_label(&format!("{:?}", weight)); - } - None => { - s.add_css_class("dim_label"); - s.set_label("No weight recorded"); - } - } - /* - *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>>) { - match new_view { - EditView::Unconfigured => {} - EditView::View(view) => self.imp().container.swap(&view.upcast()), - EditView::Edit(editor) => self.imp().container.swap(&editor.widget()), - } - /* - 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(); - } - */ -} -*/ +pub struct WeightViewPrivate {} pub struct Weight { label: gtk::Label, diff --git a/fitnesstrax/app/src/views/day_detail_view.rs b/fitnesstrax/app/src/views/day_detail_view.rs index 65bc114..cbd47b7 100644 --- a/fitnesstrax/app/src/views/day_detail_view.rs +++ b/fitnesstrax/app/src/views/day_detail_view.rs @@ -95,8 +95,12 @@ impl DayDetailView { self.imp().container.swap(&DayEdit::new( self.imp().date.borrow().clone(), self.imp().records.borrow().clone(), - |_| {}, - |_| {}, + self.on_put_record(), + self.on_update_record(), + { + let s = self.clone(); + move || s.view() + }, )); } @@ -117,17 +121,20 @@ impl DayDetailView { }) } - /* - fn on_update_record(&self, record: TraxRecord) -> dyn Fn(ft_core::TraxRecord) { + fn on_update_record(&self) -> Box)> { let app = self.imp().app.clone(); - move |record| { + Box::new(move |record| { let app = app.clone(); glib::spawn_future_local({ async move { - app.update_record(record).await; + match &*app.borrow() { + Some(app) => { + let _ = app.update_record(record).await; + } + None => {} + } } }); - } + }) } - */ } -- 2.44.1 From 56d0a536669e35807f789783c120b91576d43d9e Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Wed, 17 Jan 2024 22:35:13 -0500 Subject: [PATCH 5/7] Fix how DayEdit deals with the weight field --- fitnesstrax/app/src/components/day.rs | 8 ++++---- fitnesstrax/app/src/components/text_entry.rs | 17 +++++++++++++++-- fitnesstrax/app/src/components/weight.rs | 5 +++++ 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index c72a270..31f01f0 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -309,11 +309,11 @@ impl DayEdit { _ => None, }); - let weight_view = match weight_record { - Some((_id, data)) => WeightEdit::new(Some(data.clone())), - None => WeightEdit::new(None), + match weight_record { + Some((_id, data)) => s.imp().weight.set_value(Some(data.weight)), + None => s.imp().weight.set_value(None), }; - s.append(&weight_view.widget()); + s.append(&s.imp().weight.widget()); s } diff --git a/fitnesstrax/app/src/components/text_entry.rs b/fitnesstrax/app/src/components/text_entry.rs index 750ca2e..cc6c2a0 100644 --- a/fitnesstrax/app/src/components/text_entry.rs +++ b/fitnesstrax/app/src/components/text_entry.rs @@ -20,15 +20,25 @@ use std::{cell::RefCell, rc::Rc}; pub struct ParseError; #[derive(Clone)] -pub struct TextEntry { +pub struct TextEntry { value: Rc>>, widget: gtk::Entry, renderer: Rc String>>, parser: Rc Result>>, } +impl std::fmt::Debug for TextEntry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!( + f, + "{{ value: {:?}, widget: {:?} }}", + self.value, self.widget + ) + } +} + // I do not understand why the data should be 'static. -impl TextEntry { +impl TextEntry { pub fn new(placeholder: &str, value: Option, renderer: R, parser: V) -> Self where R: Fn(&T) -> String + 'static, @@ -63,6 +73,7 @@ impl TextEntry { } match (self.parser)(buffer.text().as_str()) { Ok(v) => { + println!("setting the value: {}", (self.renderer)(&v)); *self.value.borrow_mut() = Some(v); self.widget.remove_css_class("error"); } @@ -74,6 +85,8 @@ impl TextEntry { } pub fn value(&self) -> Option { + let v = self.value.borrow().clone(); + println!("retrieving the value: {:?}", v.map(|v| (self.renderer)(&v))); self.value.borrow().clone() } diff --git a/fitnesstrax/app/src/components/weight.rs b/fitnesstrax/app/src/components/weight.rs index e2fb631..552b107 100644 --- a/fitnesstrax/app/src/components/weight.rs +++ b/fitnesstrax/app/src/components/weight.rs @@ -48,6 +48,7 @@ impl Weight { } } +#[derive(Debug)] pub struct WeightEdit { entry: TextEntry>, } @@ -64,6 +65,10 @@ impl WeightEdit { } } + pub fn set_value(&self, value: Option>) { + self.entry.set_value(value); + } + pub fn value(&self) -> Option> { self.entry.value() } -- 2.44.1 From c075b7ed6e8366193dfe9435613cc14a3239cd20 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Wed, 17 Jan 2024 22:59:20 -0500 Subject: [PATCH 6/7] Just barely get the data saveable again Starting to see some pretty serious limitations already. --- fitnesstrax/app/src/components/day.rs | 44 +++++++++++++------- fitnesstrax/app/src/views/day_detail_view.rs | 10 ++++- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index 31f01f0..2a3d8b7 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -275,26 +275,40 @@ impl DayEdit { s.set_orientation(gtk::Orientation::Vertical); s.set_hexpand(true); - /*, move |weight| { - on_update_record(Record { - id: id.clone(), - data: ft_core::TraxRecord::Weight(ft_core::Weight { date, weight }), - }) - }*/ - - /*, move |weight| { - on_put_record(ft_core::TraxRecord::Weight(ft_core::Weight { - date, - weight, - })); - }*/ - s.append( &ActionGroup::builder() .primary_action("Save", { let s = s.clone(); + let records = records.clone(); move || { - println!("weight value: {:?}", s.imp().weight.value()); + println!("saving to database"); + let weight_record = records.iter().find_map(|record| match record { + Record { + id, + data: ft_core::TraxRecord::Weight(w), + } => Some((id, w)), + _ => None, + }); + + let weight = s.imp().weight.value(); + + if let Some(weight) = weight { + match weight_record { + Some((id, _)) => on_update_record(Record { + id: id.clone(), + data: ft_core::TraxRecord::Weight(ft_core::Weight { + date, + weight, + }), + }), + None => { + on_put_record(ft_core::TraxRecord::Weight(ft_core::Weight { + date, + weight, + })) + } + } + }; } }) .secondary_action("Cancel", on_cancel) diff --git a/fitnesstrax/app/src/views/day_detail_view.rs b/fitnesstrax/app/src/views/day_detail_view.rs index cbd47b7..f9b5128 100644 --- a/fitnesstrax/app/src/views/day_detail_view.rs +++ b/fitnesstrax/app/src/views/day_detail_view.rs @@ -54,6 +54,7 @@ impl DayDetailView { *s.imp().date.borrow_mut() = date; *s.imp().records.borrow_mut() = records; + *s.imp().app.borrow_mut() = Some(app); s.append(&s.imp().container); @@ -105,6 +106,7 @@ impl DayDetailView { } fn on_put_record(&self) -> Box { + let s = self.clone(); let app = self.imp().app.clone(); Box::new(move |record| { let app = app.clone(); @@ -118,23 +120,29 @@ impl DayDetailView { } } }); + s.view(); }) } fn on_update_record(&self) -> Box)> { + let s = self.clone(); let app = self.imp().app.clone(); Box::new(move |record| { let app = app.clone(); glib::spawn_future_local({ async move { + println!("record: {:?}", record); match &*app.borrow() { Some(app) => { let _ = app.update_record(record).await; } - None => {} + None => { + println!("no app!"); + } } } }); + s.view(); }) } } -- 2.44.1 From 1c2c4982a111fab9028df231df021f65301da085 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Thu, 18 Jan 2024 07:43:18 -0500 Subject: [PATCH 7/7] Update the record in the detail view on save --- Cargo.lock | 1 + fitnesstrax/app/Cargo.toml | 1 + fitnesstrax/app/src/app.rs | 5 ++++ fitnesstrax/app/src/components/day.rs | 1 - fitnesstrax/app/src/components/text_entry.rs | 2 -- fitnesstrax/app/src/views/day_detail_view.rs | 28 +++++++++++++++----- 6 files changed, 29 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3340ab5..c823cc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1031,6 +1031,7 @@ dependencies = [ "glib-build-tools 0.18.0", "gtk4", "libadwaita", + "thiserror", "tokio", ] diff --git a/fitnesstrax/app/Cargo.toml b/fitnesstrax/app/Cargo.toml index 980cc44..2f58d54 100644 --- a/fitnesstrax/app/Cargo.toml +++ b/fitnesstrax/app/Cargo.toml @@ -16,6 +16,7 @@ ft-core = { path = "../core" } gio = { version = "0.18" } glib = { version = "0.18" } gtk = { version = "0.7", package = "gtk4", features = [ "v4_10" ] } +thiserror = { version = "1.0" } tokio = { version = "1.34", features = [ "full" ] } [build-dependencies] diff --git a/fitnesstrax/app/src/app.rs b/fitnesstrax/app/src/app.rs index 655b2c4..94a6e00 100644 --- a/fitnesstrax/app/src/app.rs +++ b/fitnesstrax/app/src/app.rs @@ -22,11 +22,16 @@ use std::{ path::PathBuf, sync::{Arc, RwLock}, }; +use thiserror::Error; use tokio::runtime::Runtime; +#[derive(Debug, Error)] pub enum AppError { + #[error("no database loaded")] NoDatabase, + #[error("failed to open the database")] FailedToOpenDatabase, + #[error("unhandled error")] Unhandled, } diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index 2a3d8b7..1652d6c 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -281,7 +281,6 @@ impl DayEdit { let s = s.clone(); let records = records.clone(); move || { - println!("saving to database"); let weight_record = records.iter().find_map(|record| match record { Record { id, diff --git a/fitnesstrax/app/src/components/text_entry.rs b/fitnesstrax/app/src/components/text_entry.rs index cc6c2a0..996f1ed 100644 --- a/fitnesstrax/app/src/components/text_entry.rs +++ b/fitnesstrax/app/src/components/text_entry.rs @@ -73,7 +73,6 @@ impl TextEntry { } match (self.parser)(buffer.text().as_str()) { Ok(v) => { - println!("setting the value: {}", (self.renderer)(&v)); *self.value.borrow_mut() = Some(v); self.widget.remove_css_class("error"); } @@ -86,7 +85,6 @@ impl TextEntry { pub fn value(&self) -> Option { let v = self.value.borrow().clone(); - println!("retrieving the value: {:?}", v.map(|v| (self.renderer)(&v))); self.value.borrow().clone() } diff --git a/fitnesstrax/app/src/views/day_detail_view.rs b/fitnesstrax/app/src/views/day_detail_view.rs index f9b5128..c73d1f0 100644 --- a/fitnesstrax/app/src/views/day_detail_view.rs +++ b/fitnesstrax/app/src/views/day_detail_view.rs @@ -109,40 +109,56 @@ impl DayDetailView { let s = self.clone(); let app = self.imp().app.clone(); Box::new(move |record| { + let s = s.clone(); let app = app.clone(); glib::spawn_future_local({ async move { match &*app.borrow() { Some(app) => { - let _ = app.put_record(record).await; + let id = app + .put_record(record.clone()) + .await + .expect("successful write"); + s.imp() + .records + .borrow_mut() + .push(Record { id, data: record }); } None => {} } + s.view(); } }); - s.view(); }) } fn on_update_record(&self) -> Box)> { let s = self.clone(); let app = self.imp().app.clone(); - Box::new(move |record| { + Box::new(move |updated_record| { let app = app.clone(); + + let mut records = s.imp().records.borrow_mut(); + let idx = records.iter().position(|r| r.id == updated_record.id); + match idx { + Some(i) => records[i] = updated_record.clone(), + None => records.push(updated_record.clone()), + } + glib::spawn_future_local({ + let s = s.clone(); async move { - println!("record: {:?}", record); match &*app.borrow() { Some(app) => { - let _ = app.update_record(record).await; + let _ = app.update_record(updated_record).await; } None => { println!("no app!"); } } + s.view(); } }); - s.view(); }) } } -- 2.44.1