/* 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 chrono::NaiveDate; // use ft_core::TraxRecord; use crate::components::{ParseError, TextEntry}; use chrono::{Local, NaiveDate}; use dimensioned::si; use emseries::Record; use ft_core::{RecordType, TimeDistance, TraxRecord, Weight}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::{cell::RefCell, rc::Rc}; pub struct DaySummaryPrivate { date: gtk::Label, weight: RefCell>, } #[glib::object_subclass] impl ObjectSubclass for DaySummaryPrivate { const NAME: &'static str = "DaySummary"; type Type = DaySummary; type ParentType = gtk::Box; fn new() -> Self { let date = gtk::Label::builder() .css_classes(["day-summary__date"]) .halign(gtk::Align::Start) .build(); Self { date, weight: RefCell::new(None), } } } impl ObjectImpl for DaySummaryPrivate {} impl WidgetImpl for DaySummaryPrivate {} impl BoxImpl for DaySummaryPrivate {} glib::wrapper! { /// The DaySummary displays one day's activities in a narrative style. This is meant to give /// an overall feel of everything that happened during the day without going into details. pub struct DaySummary(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; } impl DaySummary { pub fn new() -> Self { let s: Self = Object::builder().build(); s.set_orientation(gtk::Orientation::Vertical); s.set_css_classes(&["day-summary"]); s.append(&s.imp().date); s } pub fn set_data(&self, date: chrono::NaiveDate, records: Vec>) { self.imp() .date .set_text(&date.format("%Y-%m-%d").to_string()); if let Some(ref weight_label) = *self.imp().weight.borrow() { self.remove(weight_label); } if let Some(Record { data: TraxRecord::Weight(weight_record), .. }) = records.iter().filter(|f| f.data.is_weight()).next() { let label = gtk::Label::builder() .halign(gtk::Align::Start) .label(&format!("{}", weight_record.weight)) .css_classes(["day-summary__weight"]) .build(); self.append(&label); *self.imp().weight.borrow_mut() = Some(label); } /* self.append( >k::Label::builder() .halign(gtk::Align::Start) .label("15km of biking in 60 minutes") .build(), ); */ } } pub struct DayDetailPrivate { date: gtk::Label, weight: RefCell>, } #[glib::object_subclass] impl ObjectSubclass for DayDetailPrivate { const NAME: &'static str = "DayDetail"; type Type = DayDetail; type ParentType = gtk::Box; fn new() -> Self { let date = gtk::Label::builder() .css_classes(["daysummary-date"]) .halign(gtk::Align::Start) .build(); Self { date, weight: RefCell::new(None), } } } impl ObjectImpl for DayDetailPrivate {} impl WidgetImpl for DayDetailPrivate {} impl BoxImpl for DayDetailPrivate {} glib::wrapper! { pub struct DayDetail(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; } impl DayDetail { pub fn new( date: chrono::NaiveDate, records: Vec>, on_put_record: PutRecordFn, on_update_record: UpdateRecordFn, ) -> Self where PutRecordFn: Fn(TraxRecord) + 'static, UpdateRecordFn: Fn(Record) + 'static, { let s: Self = Object::builder().build(); s.set_orientation(gtk::Orientation::Vertical); let click_controller = gtk::GestureClick::new(); click_controller.connect_released({ let s = s.clone(); move |_, _, _, _| { println!("clicked outside of focusable entity"); if let Some(widget) = s.focus_child().and_downcast_ref::() { println!("focused child is the weight view"); widget.blur(); } } }); s.add_controller(click_controller); let weight_record = records.iter().find_map(|record| match record { Record { id, data: 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| { on_update_record(Record { id: id.clone(), data: TraxRecord::Weight(Weight { date, weight }), }) }), None => WeightView::new(date.clone(), None, move |weight| { on_put_record(TraxRecord::Weight(Weight { date, weight })); }), }; s.append(&weight_view); records.into_iter().for_each(|record| { let record_view = match record { Record { data: TraxRecord::BikeRide(record), .. } => Some( TimeDistanceView::new(RecordType::BikeRide, record).upcast::(), ), Record { data: TraxRecord::Row(record), .. } => Some(TimeDistanceView::new(RecordType::Row, record).upcast::()), Record { data: TraxRecord::Run(record), .. } => Some(TimeDistanceView::new(RecordType::Row, record).upcast::()), Record { data: TraxRecord::Swim(record), .. } => Some(TimeDistanceView::new(RecordType::Row, record).upcast::()), Record { data: TraxRecord::Walk(record), .. } => Some(TimeDistanceView::new(RecordType::Row, record).upcast::()), _ => None, }; if let Some(record_view) = record_view { record_view.add_css_class("day-detail"); record_view.set_halign(gtk::Align::Start); s.append(&record_view); } }); s } } #[derive(Clone, Copy, PartialEq)] enum WeightViewMode { View, Edit, } pub struct WeightViewPrivate { date: RefCell, record: RefCell>, view: 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)>>, } impl Default for WeightViewPrivate { fn default() -> Self { let view = gtk::Label::builder() .css_classes(["card", "weight-view"]) .halign(gtk::Align::Start) .can_focus(true) .build(); let edit = TextEntry::>::new("weight", None, &|w: &str| { w.parse::().map(|w| w * si::KG).map_err(|_| ParseError) }); let current = view.clone(); Self { date: RefCell::new(Local::now().date_naive()), record: RefCell::new(None), 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(|_| {})), } } } #[glib::object_subclass] impl ObjectSubclass for WeightViewPrivate { const NAME: &'static str = "WeightView"; type Type = WeightView; type ParentType = gtk::Box; } impl ObjectImpl for WeightViewPrivate {} impl WidgetImpl for WeightViewPrivate {} impl BoxImpl for WeightViewPrivate {} glib::wrapper! { pub struct WeightView(ObjectSubclass) @extends gtk::Box, 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.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(); let view_click_controller = gtk::GestureClick::new(); view_click_controller.connect_released({ let s = s.clone(); move |_, _, _, _| { s.edit(); } }); s.imp().view.borrow().add_controller(view_click_controller); s } fn view(&self) { let view = self.imp().view.borrow(); 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.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.set_value(Some(record.weight)), None => edit.set_value(None), } *self.imp().mode.borrow_mut() = WeightViewMode::Edit; self.swap(edit.widget()); edit.grab_focus(); } fn swap(&self, new_view: gtk::Widget) { let mut current = self.imp().current.borrow_mut(); self.remove(&*current); self.append(&new_view); *current = new_view; } fn blur(&self) { if *self.imp().mode.borrow() == WeightViewMode::Edit { println!("on_blur"); let weight = self.imp().edit.borrow().value(); 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(); } } #[derive(Default)] pub struct TimeDistanceViewPrivate { record: RefCell>, } #[glib::object_subclass] impl ObjectSubclass for TimeDistanceViewPrivate { const NAME: &'static str = "TimeDistanceView"; type Type = TimeDistanceView; type ParentType = gtk::Box; } impl ObjectImpl for TimeDistanceViewPrivate {} impl WidgetImpl for TimeDistanceViewPrivate {} impl BoxImpl for TimeDistanceViewPrivate {} glib::wrapper! { pub struct TimeDistanceView(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; } impl TimeDistanceView { pub fn new(type_: RecordType, record: TimeDistance) -> Self { let s: Self = Object::builder().build(); s.set_orientation(gtk::Orientation::Vertical); s.set_hexpand(true); let first_row = gtk::Box::builder().homogeneous(true).build(); first_row.append( >k::Label::builder() .halign(gtk::Align::Start) .label(&record.datetime.format("%H:%M").to_string()) .build(), ); first_row.append( >k::Label::builder() .halign(gtk::Align::Start) .label(format!("{:?}", type_)) .build(), ); first_row.append( >k::Label::builder() .halign(gtk::Align::Start) .label( record .distance .map(|dist| format!("{}", dist)) .unwrap_or("".to_owned()), ) .build(), ); first_row.append( >k::Label::builder() .halign(gtk::Align::Start) .label( record .duration .map(|duration| format!("{}", duration)) .unwrap_or("".to_owned()), ) .build(), ); s.append(&first_row); s.append( >k::Label::builder() .halign(gtk::Align::Start) .label( record .comments .map(|comments| format!("{}", comments)) .unwrap_or("".to_owned()), ) .build(), ); s } }