From 3a728a51b4983ba87a46214d0534d8f38f3c2491 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Fri, 22 Dec 2023 15:08:34 -0500 Subject: [PATCH] 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 {}