From f19090311bb8808c85c14f40bc41a1891164a524 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Fri, 22 Dec 2023 14:54:38 -0500 Subject: [PATCH 1/2] Extract all of the UI components into dedicated files --- fitnesstrax/app/src/app_window.rs | 153 +++++++++ fitnesstrax/app/src/{ui => components}/mod.rs | 0 fitnesstrax/app/src/main.rs | 301 +----------------- fitnesstrax/app/src/views/historical_view.rs | 54 ++++ fitnesstrax/app/src/views/mod.rs | 47 +++ fitnesstrax/app/src/views/placeholder_view.rs | 46 +++ fitnesstrax/app/src/views/welcome_view.rs | 98 ++++++ 7 files changed, 405 insertions(+), 294 deletions(-) create mode 100644 fitnesstrax/app/src/app_window.rs rename fitnesstrax/app/src/{ui => components}/mod.rs (100%) create mode 100644 fitnesstrax/app/src/views/historical_view.rs create mode 100644 fitnesstrax/app/src/views/mod.rs create mode 100644 fitnesstrax/app/src/views/placeholder_view.rs create mode 100644 fitnesstrax/app/src/views/welcome_view.rs diff --git a/fitnesstrax/app/src/app_window.rs b/fitnesstrax/app/src/app_window.rs new file mode 100644 index 0000000..1d20043 --- /dev/null +++ b/fitnesstrax/app/src/app_window.rs @@ -0,0 +1,153 @@ +/* +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 crate::{ + views::{HistoricalView, PlaceholderView, View, ViewName, WelcomeView}, + AppInvocation, AppResponse, +}; +use adw::prelude::*; +use async_channel::Sender; +use gio::resources_lookup_data; +use glib::Object; +use gtk::STYLE_PROVIDER_PRIORITY_USER; +use gtk::{prelude::*, subclass::prelude::*}; +use std::path::PathBuf; +use std::{cell::RefCell, rc::Rc}; + +/// The application window, or the main window, is the main user interface for the app. Almost +/// everything occurs here. +#[derive(Clone)] +pub struct AppWindow { + app_tx: Sender, + window: adw::ApplicationWindow, + layout: gtk::Box, + current_view: Rc>, + settings: gio::Settings, +} + +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. + pub fn new( + app_id: &str, + resource_path: &str, + adw_app: &adw::Application, + app_tx: Sender, + ) -> AppWindow { + let window = adw::ApplicationWindow::builder() + .application(adw_app) + .width_request(800) + .height_request(600) + .build(); + + let stylesheet = String::from_utf8( + resources_lookup_data( + &format!("{}style.css", resource_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); + + let header = adw::HeaderBar::builder() + .title_widget(>k::Label::new(Some("FitnessTrax"))) + .build(); + + let layout = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .build(); + + let initial_view = View::Placeholder(PlaceholderView::new().upcast()); + + layout.append(&header); + layout.append(initial_view.widget()); + + window.set_content(Some(&layout)); + window.present(); + + let s = Self { + app_tx, + window, + layout, + current_view: Rc::new(RefCell::new(initial_view)), + settings: gio::Settings::new(app_id), + }; + + s + } + + pub fn change_view(&self, view: ViewName) { + self.swap_main(self.construct_view(view)); + } + + pub fn process_response(&self, response: AppResponse) { + match response { + AppResponse::DatabaseChanged(db_path) => { + self.settings + .set_string("series-path", db_path.to_str().unwrap()) + .unwrap(); + self.change_view(ViewName::Historical); + } + AppResponse::NoDatabase => { + self.change_view(ViewName::Welcome); + } + AppResponse::Records => { + self.change_view(ViewName::Historical); + } + } + } + + // Switch views. + // + // This function only replaces the old view with the one which matches the current view state. + // It is responsible for ensuring that the new view goes into the layout in the correct + // position. + fn swap_main(&self, view: View) { + let mut current_widget = self.current_view.borrow_mut(); + self.layout.remove(&*current_widget.widget()); + *current_widget = view; + self.layout.append(&*current_widget.widget()); + } + + fn construct_view(&self, view: ViewName) -> View { + match view { + ViewName::Placeholder => View::Placeholder(PlaceholderView::new().upcast()), + ViewName::Welcome => View::Welcome( + WelcomeView::new({ + let s = self.clone(); + Box::new(move |path: PathBuf| { + s.app_tx + .send_blocking(AppInvocation::OpenDatabase(path)) + .unwrap(); + }) + }) + .upcast(), + ), + ViewName::Historical => View::Historical(HistoricalView::new().upcast()), + } + } +} diff --git a/fitnesstrax/app/src/ui/mod.rs b/fitnesstrax/app/src/components/mod.rs similarity index 100% rename from fitnesstrax/app/src/ui/mod.rs rename to fitnesstrax/app/src/components/mod.rs diff --git a/fitnesstrax/app/src/main.rs b/fitnesstrax/app/src/main.rs index c78a69b..edb752a 100644 --- a/fitnesstrax/app/src/main.rs +++ b/fitnesstrax/app/src/main.rs @@ -14,23 +14,21 @@ 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 ui; +mod app_window; +mod components; +mod views; use adw::prelude::*; +use app_window::AppWindow; use async_channel::{Receiver, Sender}; use emseries::{EmseriesReadError, Series}; use ft_core::TraxRecord; -use gio::resources_lookup_data; -use glib::Object; -use gtk::{subclass::prelude::*, STYLE_PROVIDER_PRIORITY_USER}; use std::{ - cell::RefCell, env, path::{Path, PathBuf}, rc::Rc, sync::{Arc, RwLock}, }; -use ui::FileChooserRow; const APP_ID_DEV: &str = "com.luminescent-dreams.fitnesstrax.dev"; const APP_ID_PROD: &str = "com.luminescent-dreams.fitnesstrax"; @@ -39,7 +37,7 @@ const RESOURCE_BASE_PATH: &str = "/com/luminescent-dreams/fitnesstrax/"; /// Invocations are how parts of the application, primarily the UI, will send requests to the core. #[derive(Debug)] -enum AppInvocation { +pub enum AppInvocation { /// Tell the core to try to open a database. OpenDatabase(PathBuf), @@ -55,7 +53,7 @@ enum AppInvocation { /// A typical use would be for the UI to send an [AppInvocation::RequestRecords] request and /// receive [AppResponse::Records]. #[derive(Debug)] -enum AppResponse { +pub enum AppResponse { /// No database is available. The UI should typically display a placeholder, such as the /// welcome view. NoDatabase, @@ -111,291 +109,6 @@ impl App { } } -pub struct PlaceholderViewPrivate {} - -#[glib::object_subclass] -impl ObjectSubclass for PlaceholderViewPrivate { - const NAME: &'static str = "PlaceholderView"; - type Type = PlaceholderView; - type ParentType = gtk::Box; - - fn new() -> Self { - Self {} - } -} - -impl ObjectImpl for PlaceholderViewPrivate {} -impl WidgetImpl for PlaceholderViewPrivate {} -impl BoxImpl for PlaceholderViewPrivate {} - -glib::wrapper! { - pub struct PlaceholderView(ObjectSubclass) @extends gtk::Box, gtk::Widget; -} - -impl PlaceholderView { - pub fn new() -> Self { - let s: Self = Object::builder().build(); - s - } -} - -/// 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 WelcomeViewPrivate {} - -#[glib::object_subclass] -impl ObjectSubclass for WelcomeViewPrivate { - const NAME: &'static str = "WelcomeView"; - type Type = WelcomeView; - type ParentType = gtk::Box; - - fn new() -> Self { - Self {} - } -} - -impl ObjectImpl for WelcomeViewPrivate {} -impl WidgetImpl for WelcomeViewPrivate {} -impl BoxImpl for WelcomeViewPrivate {} - -glib::wrapper! { - pub struct WelcomeView(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; -} - -impl WelcomeView { - pub fn new(on_save: Box) -> Self - where - F: Fn(PathBuf) + 'static, - { - let s: Self = Object::builder().build(); - s.set_orientation(gtk::Orientation::Vertical); - s.set_css_classes(&["welcome"]); - - // Replace this with the welcome screen that we set up in the fitnesstrax/unconfigured-page - // branch. - let title = gtk::Label::builder() - .label("Welcome to FitnessTrax") - .css_classes(["welcome-title"]) - .build(); - - let content = gtk::Box::builder() - .css_classes(["model-content"]) - .orientation(gtk::Orientation::Vertical) - .vexpand(true) - .build(); - - let save_button = gtk::Button::builder() - .label("Save Settings") - .sensitive(false) - .build(); - - // The database selection row should be a box that shows a default database path, along with a - // button that triggers a file chooser dialog. Once the dialog returns, the box should be - // updated to reflect the chosen path. - let db_row = FileChooserRow::new({ - let save_button = save_button.clone(); - move |_| save_button.set_sensitive(true) - }); - - content.append(>k::Label::new(Some("Welcome to FitnessTrax. The application has not yet been configured, so I will walk you through that. Let's start out by selecting your database."))); - content.append(&db_row); - - let on_save = on_save; - save_button.connect_clicked({ - move |_| { - if let Some(path) = db_row.path() { - on_save(path) - } - } - }); - - s.append(&title); - s.append(&content); - s.append(&save_button); - - s - } -} - -/// 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] -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 - } -} - -#[derive(Clone, Debug, PartialEq)] -enum ViewName { - Placeholder, - Welcome, - Historical, -} - -enum View { - Placeholder(gtk::Widget), - Welcome(gtk::Widget), - Historical(gtk::Widget), -} - -impl View { - fn widget<'a>(&'a self) -> &'a gtk::Widget { - match self { - View::Placeholder(widget) => widget, - View::Welcome(widget) => widget, - View::Historical(widget) => widget, - } - } -} - -/// The application window, or the main window, is the main user interface for the app. Almost -/// everything occurs here. -#[derive(Clone)] -struct AppWindow { - app_tx: Sender, - window: adw::ApplicationWindow, - layout: gtk::Box, - current_view: Rc>, - settings: gio::Settings, -} - -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(app_id: &str, adw_app: &adw::Application, app_tx: Sender) -> AppWindow { - let window = adw::ApplicationWindow::builder() - .application(adw_app) - .width_request(800) - .height_request(600) - .build(); - - 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); - - let header = adw::HeaderBar::builder() - .title_widget(>k::Label::new(Some("FitnessTrax"))) - .build(); - - let layout = gtk::Box::builder() - .orientation(gtk::Orientation::Vertical) - .build(); - - let initial_view = View::Placeholder(PlaceholderView::new().upcast()); - - layout.append(&header); - layout.append(initial_view.widget()); - - window.set_content(Some(&layout)); - window.present(); - - let s = Self { - app_tx, - window, - layout, - current_view: Rc::new(RefCell::new(initial_view)), - settings: gio::Settings::new(app_id), - }; - - s - } - - pub fn change_view(&self, view: ViewName) { - self.swap_main(self.construct_view(view)); - } - - pub fn process_response(&self, response: AppResponse) { - match response { - AppResponse::DatabaseChanged(db_path) => { - self.settings - .set_string("series-path", db_path.to_str().unwrap()) - .unwrap(); - self.change_view(ViewName::Historical); - } - AppResponse::NoDatabase => { - self.change_view(ViewName::Welcome); - } - AppResponse::Records => { - self.change_view(ViewName::Historical); - } - } - } - - // Switch views. - // - // This function only replaces the old view with the one which matches the current view state. - // It is responsible for ensuring that the new view goes into the layout in the correct - // position. - fn swap_main(&self, view: View) { - let mut current_widget = self.current_view.borrow_mut(); - self.layout.remove(&*current_widget.widget()); - *current_widget = view; - self.layout.append(&*current_widget.widget()); - } - - fn construct_view(&self, view: ViewName) -> View { - match view { - ViewName::Placeholder => View::Placeholder(PlaceholderView::new().upcast()), - ViewName::Welcome => View::Welcome( - WelcomeView::new({ - let s = self.clone(); - Box::new(move |path: PathBuf| { - s.app_tx - .send_blocking(AppInvocation::OpenDatabase(path)) - .unwrap(); - }) - }) - .upcast(), - ), - ViewName::Historical => View::Historical(HistoricalView::new().upcast()), - } - } -} - fn main() { // I still don't fully understand gio resources. resources_register_include! is convenient // because I don't have to deal with filesystem locations at runtime. However, I think other @@ -446,7 +159,7 @@ fn main() { // UI. let (app_tx, app_rx) = async_channel::unbounded::(); - let window = AppWindow::new(app_id, adw_app, app_tx.clone()); + let window = AppWindow::new(app_id, RESOURCE_BASE_PATH, adw_app, app_tx.clone()); // Spawn a future where the UI will receive messages for the app window. Previously, this // would have been done by creating a glib::MainContext::channel(), but that has been diff --git a/fitnesstrax/app/src/views/historical_view.rs b/fitnesstrax/app/src/views/historical_view.rs new file mode 100644 index 0000000..b69e960 --- /dev/null +++ b/fitnesstrax/app/src/views/historical_view.rs @@ -0,0 +1,54 @@ +/* +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 glib::Object; +use gtk::{prelude::*, subclass::prelude::*}; + +/// 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] +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 + } +} diff --git a/fitnesstrax/app/src/views/mod.rs b/fitnesstrax/app/src/views/mod.rs new file mode 100644 index 0000000..ecbdea3 --- /dev/null +++ b/fitnesstrax/app/src/views/mod.rs @@ -0,0 +1,47 @@ +/* +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 . +*/ + +mod historical_view; +pub use historical_view::HistoricalView; + +mod placeholder_view; +pub use placeholder_view::PlaceholderView; + +mod welcome_view; +pub use welcome_view::WelcomeView; + +#[derive(Clone, Debug, PartialEq)] +pub enum ViewName { + Placeholder, + Welcome, + Historical, +} + +pub enum View { + Placeholder(gtk::Widget), + Welcome(gtk::Widget), + Historical(gtk::Widget), +} + +impl View { + pub fn widget<'a>(&'a self) -> &'a gtk::Widget { + match self { + View::Placeholder(widget) => widget, + View::Welcome(widget) => widget, + View::Historical(widget) => widget, + } + } +} diff --git a/fitnesstrax/app/src/views/placeholder_view.rs b/fitnesstrax/app/src/views/placeholder_view.rs new file mode 100644 index 0000000..8ce2804 --- /dev/null +++ b/fitnesstrax/app/src/views/placeholder_view.rs @@ -0,0 +1,46 @@ +/* +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 glib::Object; +use gtk::{prelude::*, subclass::prelude::*}; + +pub struct PlaceholderViewPrivate {} + +#[glib::object_subclass] +impl ObjectSubclass for PlaceholderViewPrivate { + const NAME: &'static str = "PlaceholderView"; + type Type = PlaceholderView; + type ParentType = gtk::Box; + + fn new() -> Self { + Self {} + } +} + +impl ObjectImpl for PlaceholderViewPrivate {} +impl WidgetImpl for PlaceholderViewPrivate {} +impl BoxImpl for PlaceholderViewPrivate {} + +glib::wrapper! { + pub struct PlaceholderView(ObjectSubclass) @extends gtk::Box, gtk::Widget; +} + +impl PlaceholderView { + pub fn new() -> Self { + let s: Self = Object::builder().build(); + s + } +} diff --git a/fitnesstrax/app/src/views/welcome_view.rs b/fitnesstrax/app/src/views/welcome_view.rs new file mode 100644 index 0000000..5156c9c --- /dev/null +++ b/fitnesstrax/app/src/views/welcome_view.rs @@ -0,0 +1,98 @@ +/* +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 crate::components::FileChooserRow; +use glib::Object; +use gtk::{prelude::*, subclass::prelude::*}; +use std::path::PathBuf; + +/// 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 WelcomeViewPrivate {} + +#[glib::object_subclass] +impl ObjectSubclass for WelcomeViewPrivate { + const NAME: &'static str = "WelcomeView"; + type Type = WelcomeView; + type ParentType = gtk::Box; + + fn new() -> Self { + Self {} + } +} + +impl ObjectImpl for WelcomeViewPrivate {} +impl WidgetImpl for WelcomeViewPrivate {} +impl BoxImpl for WelcomeViewPrivate {} + +glib::wrapper! { + pub struct WelcomeView(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; +} + +impl WelcomeView { + pub fn new(on_save: Box) -> Self + where + F: Fn(PathBuf) + 'static, + { + let s: Self = Object::builder().build(); + s.set_orientation(gtk::Orientation::Vertical); + s.set_css_classes(&["welcome"]); + + // Replace this with the welcome screen that we set up in the fitnesstrax/unconfigured-page + // branch. + let title = gtk::Label::builder() + .label("Welcome to FitnessTrax") + .css_classes(["welcome-title"]) + .build(); + + let content = gtk::Box::builder() + .css_classes(["model-content"]) + .orientation(gtk::Orientation::Vertical) + .vexpand(true) + .build(); + + let save_button = gtk::Button::builder() + .label("Save Settings") + .sensitive(false) + .build(); + + // The database selection row should be a box that shows a default database path, along with a + // button that triggers a file chooser dialog. Once the dialog returns, the box should be + // updated to reflect the chosen path. + let db_row = FileChooserRow::new({ + let save_button = save_button.clone(); + move |_| save_button.set_sensitive(true) + }); + + content.append(>k::Label::new(Some("Welcome to FitnessTrax. The application has not yet been configured, so I will walk you through that. Let's start out by selecting your database."))); + content.append(&db_row); + + let on_save = on_save; + save_button.connect_clicked({ + move |_| { + if let Some(path) = db_row.path() { + on_save(path) + } + } + }); + + s.append(&title); + s.append(&content); + s.append(&save_button); + + s + } +} -- 2.44.1 From 3a728a51b4983ba87a46214d0534d8f38f3c2491 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Fri, 22 Dec 2023 15:08:34 -0500 Subject: [PATCH 2/2] Extract the application loop from the main file --- fitnesstrax/app/src/app.rs | 93 ++++++++++++++++++ fitnesstrax/app/src/app_window.rs | 9 +- fitnesstrax/app/src/components/mod.rs | 6 +- fitnesstrax/app/src/main.rs | 95 ++----------------- fitnesstrax/app/src/views/mod.rs | 1 - fitnesstrax/app/src/views/placeholder_view.rs | 2 +- 6 files changed, 105 insertions(+), 101 deletions(-) create mode 100644 fitnesstrax/app/src/app.rs diff --git a/fitnesstrax/app/src/app.rs b/fitnesstrax/app/src/app.rs new file mode 100644 index 0000000..8482fd1 --- /dev/null +++ b/fitnesstrax/app/src/app.rs @@ -0,0 +1,93 @@ +/* +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 emseries::Series; +use ft_core::TraxRecord; +use std::{ + path::{Path, PathBuf}, + sync::{Arc, RwLock}, +}; + +/// Invocations are how parts of the application, primarily the UI, will send requests to the core. +#[derive(Debug)] +pub enum AppInvocation { + /// Tell the core to try to open a database. + OpenDatabase(PathBuf), + + /// Request a set of records from the core. + // Note: this will require a time range, but doesn't yet. + RequestRecords, +} + +/// Responses are messages that the core sends to the UI. Though they are called responses, the +/// could actually be pre-emptively sent, such as notifications. The UI will need to be able to +/// process those any time they arrive. +/// +/// A typical use would be for the UI to send an [AppInvocation::RequestRecords] request and +/// receive [AppResponse::Records]. +#[derive(Debug)] +pub enum AppResponse { + /// No database is available. The UI should typically display a placeholder, such as the + /// welcome view. + NoDatabase, + + /// The database is open and here is a set of records. Typically, the set of records will be + /// all of the records within a time frame, but this can actually be any set of records. + Records, + + /// The database has been changed. This message is useful for telling the UI that a significant + /// change has happened. Further, the UI needs to save PathBuf to settings, because the + /// gio::Settings system can't be run in the fully async background. + DatabaseChanged(PathBuf), +} + +/// The real, headless application. This is where all of the logic will reside. +#[derive(Clone)] +pub struct App { + database: Arc>>>, +} + +impl App { + pub fn new(db_path: Option) -> Self { + let database = db_path.map(|path| Series::open(path).unwrap()); + let s = Self { + database: Arc::new(RwLock::new(database)), + }; + + s + } + + pub async fn process_invocation(&self, invocation: AppInvocation) -> AppResponse { + match invocation { + AppInvocation::OpenDatabase(db_path) => { + self.open_db(&db_path); + AppResponse::DatabaseChanged(db_path) + } + AppInvocation::RequestRecords => { + if self.database.read().unwrap().is_none() { + AppResponse::NoDatabase + } else { + AppResponse::Records + } + } + } + } + + fn open_db(&self, path: &Path) { + let db = Series::open(path).unwrap(); + *self.database.write().unwrap() = Some(db); + } +} diff --git a/fitnesstrax/app/src/app_window.rs b/fitnesstrax/app/src/app_window.rs index 1d20043..c6d834b 100644 --- a/fitnesstrax/app/src/app_window.rs +++ b/fitnesstrax/app/src/app_window.rs @@ -15,15 +15,13 @@ You should have received a copy of the GNU General Public License along with Fit */ use crate::{ + app::{AppInvocation, AppResponse}, views::{HistoricalView, PlaceholderView, View, ViewName, WelcomeView}, - AppInvocation, AppResponse, }; use adw::prelude::*; use async_channel::Sender; use gio::resources_lookup_data; -use glib::Object; use gtk::STYLE_PROVIDER_PRIORITY_USER; -use gtk::{prelude::*, subclass::prelude::*}; use std::path::PathBuf; use std::{cell::RefCell, rc::Rc}; @@ -32,7 +30,6 @@ use std::{cell::RefCell, rc::Rc}; #[derive(Clone)] pub struct AppWindow { app_tx: Sender, - window: adw::ApplicationWindow, layout: gtk::Box, current_view: Rc>, settings: gio::Settings, @@ -70,7 +67,9 @@ impl AppWindow { let provider = gtk::CssProvider::new(); provider.load_from_data(&stylesheet); + #[allow(deprecated)] let context = window.style_context(); + #[allow(deprecated)] context.add_provider(&provider, STYLE_PROVIDER_PRIORITY_USER); let header = adw::HeaderBar::builder() @@ -91,7 +90,6 @@ impl AppWindow { let s = Self { app_tx, - window, layout, current_view: Rc::new(RefCell::new(initial_view)), settings: gio::Settings::new(app_id), @@ -135,7 +133,6 @@ impl AppWindow { fn construct_view(&self, view: ViewName) -> View { match view { - ViewName::Placeholder => View::Placeholder(PlaceholderView::new().upcast()), ViewName::Welcome => View::Welcome( WelcomeView::new({ let s = self.clone(); diff --git a/fitnesstrax/app/src/components/mod.rs b/fitnesstrax/app/src/components/mod.rs index 14f1243..7fcba76 100644 --- a/fitnesstrax/app/src/components/mod.rs +++ b/fitnesstrax/app/src/components/mod.rs @@ -16,11 +16,7 @@ You should have received a copy of the GNU General Public License along with Fit use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; -use std::{ - cell::RefCell, - path::{Path, PathBuf}, - rc::Rc, -}; +use std::{cell::RefCell, path::PathBuf, rc::Rc}; pub struct FileChooserRowPrivate { path: RefCell>, diff --git a/fitnesstrax/app/src/main.rs b/fitnesstrax/app/src/main.rs index edb752a..6bcd8e7 100644 --- a/fitnesstrax/app/src/main.rs +++ b/fitnesstrax/app/src/main.rs @@ -14,101 +14,20 @@ 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 app; mod app_window; mod components; mod views; use adw::prelude::*; use app_window::AppWindow; -use async_channel::{Receiver, Sender}; -use emseries::{EmseriesReadError, Series}; -use ft_core::TraxRecord; -use std::{ - env, - path::{Path, PathBuf}, - rc::Rc, - sync::{Arc, RwLock}, -}; +use std::{env, path::PathBuf}; 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/"; -/// Invocations are how parts of the application, primarily the UI, will send requests to the core. -#[derive(Debug)] -pub enum AppInvocation { - /// Tell the core to try to open a database. - OpenDatabase(PathBuf), - - /// Request a set of records from the core. - // Note: this will require a time range, but doesn't yet. - RequestRecords, -} - -/// Responses are messages that the core sends to the UI. Though they are called responses, the -/// could actually be pre-emptively sent, such as notifications. The UI will need to be able to -/// process those any time they arrive. -/// -/// A typical use would be for the UI to send an [AppInvocation::RequestRecords] request and -/// receive [AppResponse::Records]. -#[derive(Debug)] -pub enum AppResponse { - /// No database is available. The UI should typically display a placeholder, such as the - /// welcome view. - NoDatabase, - - /// The database is open and here is a set of records. Typically, the set of records will be - /// all of the records within a time frame, but this can actually be any set of records. - Records, - - /// The database has been changed. This message is useful for telling the UI that a significant - /// change has happened. Further, the UI needs to save PathBuf to settings, because the - /// gio::Settings system can't be run in the fully async background. - DatabaseChanged(PathBuf), -} - -// Note that I have not yet figured out the communication channel or how the central dispatcher -// should work. There's a dance between the App and the AppWindow that I haven't figured out yet. - -/// The real, headless application. This is where all of the logic will reside. -#[derive(Clone)] -struct App { - database: Arc>>>, -} - -impl App { - pub fn new(db_path: Option) -> Self { - let database = db_path.map(|path| Series::open(path).unwrap()); - let s = Self { - database: Arc::new(RwLock::new(database)), - }; - - s - } - - pub async fn process_invocation(&self, invocation: AppInvocation) -> AppResponse { - match invocation { - AppInvocation::OpenDatabase(db_path) => { - self.open_db(&db_path); - AppResponse::DatabaseChanged(db_path) - } - AppInvocation::RequestRecords => { - if self.database.read().unwrap().is_none() { - AppResponse::NoDatabase - } else { - AppResponse::Records - } - } - } - } - - pub fn open_db(&self, path: &Path) { - let db = Series::open(path).unwrap(); - *self.database.write().unwrap() = Some(db); - } -} - fn main() { // I still don't fully understand gio resources. resources_register_include! is convenient // because I don't have to deal with filesystem locations at runtime. However, I think other @@ -124,7 +43,7 @@ fn main() { }; let settings = gio::Settings::new(app_id); - let app = App::new({ + let app = app::App::new({ let path = settings.string("series-path"); if path.is_empty() { None @@ -146,18 +65,18 @@ fn main() { adw_app.connect_activate(move |adw_app| { // These channels are used to send messages to the UI. Anything that needs to send a // message to the UI will send it via `ui_tx`. We will have one single process that owns - // `ui_rx`. That process will read messages coming in and send them to AppWindow for proper + // `ui_rx`. That process will read messages coming in and send them to [AppWindow] for proper // processing. // // The core app will usually only send messages in response to a request, but this channel // can also be used to tell the UI that something happened in the background, such as // detecting a watch, detecting new tracks to import, and so forth. - let (ui_tx, ui_rx) = async_channel::unbounded::(); + let (ui_tx, ui_rx) = async_channel::unbounded::(); // These channels are used for communicating with the app. Already I can see that a lot of // different event handlers will need copies of app_tx in order to send requests into the // UI. - let (app_tx, app_rx) = async_channel::unbounded::(); + let (app_tx, app_rx) = async_channel::unbounded::(); let window = AppWindow::new(app_id, RESOURCE_BASE_PATH, adw_app, app_tx.clone()); @@ -167,7 +86,7 @@ fn main() { glib::spawn_future_local(async move { // The app requests data to start with. This kicks everything off. The response from // the app will cause the window to be updated shortly. - let _ = app_tx.send(AppInvocation::RequestRecords).await; + let _ = app_tx.send(app::AppInvocation::RequestRecords).await; while let Ok(response) = ui_rx.recv().await { window.process_response(response); diff --git a/fitnesstrax/app/src/views/mod.rs b/fitnesstrax/app/src/views/mod.rs index ecbdea3..511d2ce 100644 --- a/fitnesstrax/app/src/views/mod.rs +++ b/fitnesstrax/app/src/views/mod.rs @@ -25,7 +25,6 @@ pub use welcome_view::WelcomeView; #[derive(Clone, Debug, PartialEq)] pub enum ViewName { - Placeholder, Welcome, Historical, } diff --git a/fitnesstrax/app/src/views/placeholder_view.rs b/fitnesstrax/app/src/views/placeholder_view.rs index 8ce2804..c3f68eb 100644 --- a/fitnesstrax/app/src/views/placeholder_view.rs +++ b/fitnesstrax/app/src/views/placeholder_view.rs @@ -15,7 +15,7 @@ You should have received a copy of the GNU General Public License along with Fit */ use glib::Object; -use gtk::{prelude::*, subclass::prelude::*}; +use gtk::subclass::prelude::*; pub struct PlaceholderViewPrivate {} -- 2.44.1