From baf652173c069e3bc55657bf557d6e3e1d92876c Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 18 Dec 2023 18:30:41 -0500 Subject: [PATCH 1/3] Set up the main views for the window, as well as the redraw policy Whenever we change views, we need to call the redraw function. That function will handle dropping the old view and populating the new one. --- Cargo.lock | 1 + fitnesstrax/app/Cargo.toml | 1 + fitnesstrax/app/src/main.rs | 191 ++++++++++++++++++++++++++++++------ fitnesstrax/core/src/lib.rs | 1 + 4 files changed, 166 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3c04fd4..4b85df9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -976,6 +976,7 @@ checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" name = "fitnesstrax" version = "0.1.0" dependencies = [ + "emseries", "ft-core", "gio", "glib", diff --git a/fitnesstrax/app/Cargo.toml b/fitnesstrax/app/Cargo.toml index 005fc16..470ecea 100644 --- a/fitnesstrax/app/Cargo.toml +++ b/fitnesstrax/app/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] adw = { version = "0.5", package = "libadwaita", features = [ "v1_2" ] } +emseries = { path = "../../emseries" } ft-core = { path = "../core" } gio = { version = "0.18" } glib = { version = "0.18" } diff --git a/fitnesstrax/app/src/main.rs b/fitnesstrax/app/src/main.rs index 570c7c9..8875bef 100644 --- a/fitnesstrax/app/src/main.rs +++ b/fitnesstrax/app/src/main.rs @@ -1,16 +1,171 @@ +use adw::prelude::*; +use emseries::Series; +use ft_core::TraxRecord; use gio::resources_lookup_data; -use gtk::{prelude::*, STYLE_PROVIDER_PRIORITY_USER}; -use std::env; +use glib::Object; +use gtk::{prelude::*, subclass::prelude::*, STYLE_PROVIDER_PRIORITY_USER}; +use std::{ + cell::RefCell, + env, + sync::{Arc, RwLock}, +}; const APP_ID_DEV: &str = "com.luminescent-dreams.fitnesstrax.dev"; const APP_ID_PROD: &str = "com.luminescent-dreams.fitnesstrax"; const RESOURCE_BASE_PATH: &str = "/com/luminescent-dreams/fitnesstrax/"; -struct AppState {} +/// The real, headless application. This is where all of the logic will reside. +#[derive(Clone)] +struct App { + database: Arc>>>, +} +impl App { + pub fn new() -> Self { + Self { + database: Arc::new(RwLock::new(None)), + } + } +} + +pub struct UnconfiguredViewPrivate {} + +#[glib::object_subclass] +impl ObjectSubclass for UnconfiguredViewPrivate { + const NAME: &'static str = "UnconfiguredView"; + type Type = UnconfiguredView; + type ParentType = gtk::Box; + + fn new() -> Self { + Self {} + } +} + +impl ObjectImpl for UnconfiguredViewPrivate {} +impl WidgetImpl for UnconfiguredViewPrivate {} +impl BoxImpl for UnconfiguredViewPrivate {} + +glib::wrapper! { + pub struct UnconfiguredView(ObjectSubclass) @extends gtk::Box, gtk::Widget; +} + +impl UnconfiguredView { + pub fn new() -> Self { + let s: Self = Object::builder().build(); + + let label = gtk::Label::builder() + .label("Database is not configured.") + .build(); + s.append(&label); + s + } +} + +pub struct HistoricalViewPrivate {} + +#[glib::object_subclass] +impl ObjectSubclass for HistoricalViewPrivate { + const NAME: &'static str = "HistoricalView"; + type Type = HistoricalView; + type ParentType = gtk::Box; + + fn new() -> Self { + Self {} + } +} + +impl ObjectImpl for HistoricalViewPrivate {} +impl WidgetImpl for HistoricalViewPrivate {} +impl BoxImpl for HistoricalViewPrivate {} + +glib::wrapper! { + pub struct HistoricalView(ObjectSubclass) @extends gtk::Box, gtk::Widget; +} + +impl HistoricalView { + pub fn new() -> Self { + let s: Self = Object::builder().build(); + + let label = gtk::Label::builder() + .label("Database has been configured and now it is time to show data") + .build(); + s.append(&label); + s + } +} + +/// These are the possible states of the main application view. +enum MainView { + /// The application is not configured yet. This is a basic background widget to take up the + /// space when there is no data to be shown. + Unconfigured(UnconfiguredView), + + /// The Historical view shows a history of records and whatnot. + Historical(HistoricalView), +} + +/// The application window, or the main window, is the main user interface for the app. Almost +/// everything occurs here. struct AppWindow { + app: App, window: adw::ApplicationWindow, + current_view: RefCell, +} + +impl AppWindow { + /// Construct a new App Window. + /// + /// adw_app is an Adwaita application. Application windows need to have access to this, but + /// otherwise we don't use this. + /// + /// app is a core [App] object which encapsulates all of the basic logic. + fn new(adw_app: &adw::Application, app: App) -> AppWindow { + let window = adw::ApplicationWindow::builder() + .application(adw_app) + .width_request(800) + .height_request(600) + .build(); + + let current_view = if app.database.read().unwrap().is_none() { + MainView::Unconfigured(UnconfiguredView::new()) + } else { + MainView::Historical(HistoricalView::new()) + }; + + let stylesheet = String::from_utf8( + resources_lookup_data( + &format!("{}style.css", RESOURCE_BASE_PATH), + gio::ResourceLookupFlags::NONE, + ) + .expect("stylesheet must be available in the resources") + .to_vec(), + ) + .expect("to parse stylesheet"); + + let provider = gtk::CssProvider::new(); + provider.load_from_data(&stylesheet); + + let context = window.style_context(); + context.add_provider(&provider, STYLE_PROVIDER_PRIORITY_USER); + + window.present(); + + let s = Self { + app, + window, + current_view: RefCell::new(current_view), + }; + s.redraw(); + s + } + + fn redraw(&self) { + match *self.current_view.borrow() { + MainView::Unconfigured(ref view) => self.window.set_content(Some(view)), + MainView::Historical(ref view) => self.window.set_content(Some(view)), + } + } } fn main() { @@ -31,10 +186,7 @@ fn main() { println!("database path: {}", settings.string("series-path")); - let app = adw::Application::builder() - .application_id(app_id) - .resource_base_path(RESOURCE_BASE_PATH) - .build(); + let app = App::new(); /* let runtime = tokio::runtime::Builder::new_multi_thread() @@ -43,32 +195,15 @@ fn main() { .unwrap(); */ - let app = adw::Application::builder() + let adw_app = adw::Application::builder() .application_id(app_id) .resource_base_path(RESOURCE_BASE_PATH) .build(); - app.connect_activate(move |app| { - let stylesheet = String::from_utf8( - resources_lookup_data( - &format!("{}style.css", RESOURCE_BASE_PATH), - gio::ResourceLookupFlags::NONE, - ) - .expect("stylesheet must be available in the resources") - .to_vec(), - ) - .expect("to parse stylesheet"); - - let provider = gtk::CssProvider::new(); - provider.load_from_data(&stylesheet); - - let window = adw::ApplicationWindow::new(app); - let context = window.style_context(); - context.add_provider(&provider, STYLE_PROVIDER_PRIORITY_USER); - - window.present(); + adw_app.connect_activate(move |adw_app| { + AppWindow::new(adw_app, app.clone()); }); let args: Vec = env::args().collect(); - ApplicationExtManual::run_with_args(&app, &args); + ApplicationExtManual::run_with_args(&adw_app, &args); } diff --git a/fitnesstrax/core/src/lib.rs b/fitnesstrax/core/src/lib.rs index b591639..9d7103a 100644 --- a/fitnesstrax/core/src/lib.rs +++ b/fitnesstrax/core/src/lib.rs @@ -4,3 +4,4 @@ use emseries::DateTimeTz; mod legacy; mod types; +pub use types::TraxRecord; -- 2.44.1 From 0ebdcd7c2aefe67669d93cf3c613741f6a661881 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 18 Dec 2023 18:36:22 -0500 Subject: [PATCH 2/3] Add some commentary --- fitnesstrax/app/src/main.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/fitnesstrax/app/src/main.rs b/fitnesstrax/app/src/main.rs index 8875bef..ad2c6d5 100644 --- a/fitnesstrax/app/src/main.rs +++ b/fitnesstrax/app/src/main.rs @@ -1,3 +1,18 @@ +/* +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 adw::prelude::*; use emseries::Series; use ft_core::TraxRecord; @@ -29,6 +44,8 @@ impl App { } } +/// This is the view to show if the application has not yet been configured. It will walk the user +/// through the most critical setup steps so that we can move on to the other views in the app. pub struct UnconfiguredViewPrivate {} #[glib::object_subclass] @@ -54,6 +71,8 @@ impl UnconfiguredView { pub fn new() -> Self { let s: Self = Object::builder().build(); + // Replace this with the welcome screen that we set up in the fitnesstrax/unconfigured-page + // branch. let label = gtk::Label::builder() .label("Database is not configured.") .build(); @@ -62,6 +81,9 @@ impl UnconfiguredView { } } +/// The historical view will show a window into the main database. It will show some version of +/// daily summaries, daily details, and will provide all functions the user may need for editing +/// records. pub struct HistoricalViewPrivate {} #[glib::object_subclass] @@ -160,6 +182,10 @@ impl AppWindow { s } + // Switch views. + // + // This function only replaces the old view with the one which matches the current view state. + // If there is any other setup/teardown to do, it will handle that as well. fn redraw(&self) { match *self.current_view.borrow() { MainView::Unconfigured(ref view) => self.window.set_content(Some(view)), -- 2.44.1 From acdf9ec1508ecb02c337e9443ca92c315b8c632b Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 18 Dec 2023 19:08:32 -0500 Subject: [PATCH 3/3] Add the window header bar --- fitnesstrax/app/src/main.rs | 45 ++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/fitnesstrax/app/src/main.rs b/fitnesstrax/app/src/main.rs index ad2c6d5..dcb3748 100644 --- a/fitnesstrax/app/src/main.rs +++ b/fitnesstrax/app/src/main.rs @@ -18,7 +18,7 @@ use emseries::Series; use ft_core::TraxRecord; use gio::resources_lookup_data; use glib::Object; -use gtk::{prelude::*, subclass::prelude::*, STYLE_PROVIDER_PRIORITY_USER}; +use gtk::{subclass::prelude::*, STYLE_PROVIDER_PRIORITY_USER}; use std::{ cell::RefCell, env, @@ -117,22 +117,13 @@ impl HistoricalView { } } -/// These are the possible states of the main application view. -enum MainView { - /// The application is not configured yet. This is a basic background widget to take up the - /// space when there is no data to be shown. - Unconfigured(UnconfiguredView), - - /// The Historical view shows a history of records and whatnot. - Historical(HistoricalView), -} - /// The application window, or the main window, is the main user interface for the app. Almost /// everything occurs here. struct AppWindow { app: App, window: adw::ApplicationWindow, - current_view: RefCell, + layout: gtk::Box, + current_view: RefCell, } impl AppWindow { @@ -150,9 +141,9 @@ impl AppWindow { .build(); let current_view = if app.database.read().unwrap().is_none() { - MainView::Unconfigured(UnconfiguredView::new()) + UnconfiguredView::new().upcast() } else { - MainView::Historical(HistoricalView::new()) + HistoricalView::new().upcast() }; let stylesheet = String::from_utf8( @@ -171,26 +162,38 @@ impl AppWindow { let context = window.style_context(); context.add_provider(&provider, STYLE_PROVIDER_PRIORITY_USER); + let header = adw::HeaderBar::builder() + .title_widget(>k::Label::new(Some("FitnessTrax"))) + .build(); + + let layout = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .build(); + layout.append(&header); + layout.append(¤t_view); + + window.set_content(Some(&layout)); window.present(); let s = Self { app, window, + layout, current_view: RefCell::new(current_view), }; - s.redraw(); s } // Switch views. // // This function only replaces the old view with the one which matches the current view state. - // If there is any other setup/teardown to do, it will handle that as well. - fn redraw(&self) { - match *self.current_view.borrow() { - MainView::Unconfigured(ref view) => self.window.set_content(Some(view)), - MainView::Historical(ref view) => self.window.set_content(Some(view)), - } + // It is responsible for ensuring that the new view goes into the layout in the correct + // position. + fn change_view(&self, view: gtk::Widget) { + let mut current_view = self.current_view.borrow_mut(); + self.layout.remove(&*current_view); + *current_view = view; + self.layout.append(&*current_view); } } -- 2.44.1