From 1e6555ef6187401439d0061f7ff3ec2da85a0d01 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 15 Jan 2024 15:53:01 -0500 Subject: [PATCH] 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;