Create the asynchronous communication channel between the UI and the core app loop. #126

Merged
savanni merged 4 commits from fitnesstrax/async-handlers into main 2023-12-22 20:16:56 +00:00
1 changed files with 118 additions and 68 deletions
Showing only changes of commit 9c200f555c - Show all commits

View File

@ -40,11 +40,14 @@ const RESOURCE_BASE_PATH: &str = "/com/luminescent-dreams/fitnesstrax/";
#[derive(Debug)] #[derive(Debug)]
enum AppInvocation { enum AppInvocation {
OpenDatabase(PathBuf), OpenDatabase(PathBuf),
RequestRecords,
} }
#[derive(Debug)] #[derive(Debug)]
enum AppResponse { enum AppResponse {
DatabaseChanged, NoDatabase,
Records,
DatabaseChanged(PathBuf),
} }
// Note that I have not yet figured out the communication channel or how the central dispatcher // Note that I have not yet figured out the communication channel or how the central dispatcher
@ -53,32 +56,38 @@ enum AppResponse {
/// The real, headless application. This is where all of the logic will reside. /// The real, headless application. This is where all of the logic will reside.
#[derive(Clone)] #[derive(Clone)]
struct App { struct App {
settings: gio::Settings,
database: Arc<RwLock<Option<Series<TraxRecord>>>>, database: Arc<RwLock<Option<Series<TraxRecord>>>>,
} }
impl App { impl App {
pub fn new(settings: gio::Settings) -> Self { pub fn new(db_path: Option<PathBuf>) -> Self {
let database = db_path.map(|path| Series::open(path).unwrap());
let s = Self { let s = Self {
settings, database: Arc::new(RwLock::new(database)),
database: Arc::new(RwLock::new(None)),
}; };
if !s.settings.string("series-path").is_empty() {
let path = PathBuf::from(s.settings.string("series-path"));
let db = Series::open(path).unwrap();
*s.database.write().unwrap() = Some(db);
}
s 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) { pub fn open_db(&self, path: &Path) {
let db = Series::open(path).unwrap(); let db = Series::open(path).unwrap();
*self.database.write().unwrap() = Some(db); *self.database.write().unwrap() = Some(db);
self.settings
.set_string("series-path", path.to_str().unwrap())
.unwrap();
} }
} }
@ -224,6 +233,29 @@ impl HistoricalView {
} }
} }
#[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 /// The application window, or the main window, is the main user interface for the app. Almost
/// everything occurs here. /// everything occurs here.
#[derive(Clone)] #[derive(Clone)]
@ -231,7 +263,8 @@ struct AppWindow {
app_tx: Sender<AppInvocation>, app_tx: Sender<AppInvocation>,
window: adw::ApplicationWindow, window: adw::ApplicationWindow,
layout: gtk::Box, layout: gtk::Box,
current_view: Rc<RefCell<gtk::Widget>>, current_view: Rc<RefCell<View>>,
settings: gio::Settings,
} }
impl AppWindow { impl AppWindow {
@ -241,7 +274,7 @@ impl AppWindow {
/// otherwise we don't use this. /// otherwise we don't use this.
/// ///
/// app is a core [App] object which encapsulates all of the basic logic. /// app is a core [App] object which encapsulates all of the basic logic.
fn new(adw_app: &adw::Application, app_tx: Sender<AppInvocation>) -> AppWindow { fn new(app_id: &str, adw_app: &adw::Application, app_tx: Sender<AppInvocation>) -> AppWindow {
let window = adw::ApplicationWindow::builder() let window = adw::ApplicationWindow::builder()
.application(adw_app) .application(adw_app)
.width_request(800) .width_request(800)
@ -272,10 +305,10 @@ impl AppWindow {
.orientation(gtk::Orientation::Vertical) .orientation(gtk::Orientation::Vertical)
.build(); .build();
let initial_view = PlaceholderView::new(); let initial_view = View::Placeholder(PlaceholderView::new().upcast());
layout.append(&header); layout.append(&header);
layout.append(&initial_view); layout.append(initial_view.widget());
window.set_content(Some(&layout)); window.set_content(Some(&layout));
window.present(); window.present();
@ -284,58 +317,62 @@ impl AppWindow {
app_tx, app_tx,
window, window,
layout, layout,
current_view: Rc::new(RefCell::new(initial_view.upcast())), current_view: Rc::new(RefCell::new(initial_view)),
settings: gio::Settings::new(app_id),
}; };
let initial_view = if true {
WelcomeView::new({
let s = s.clone();
Box::new(move |path: PathBuf| {
// The user has selected a path. Perhaps the path is new, perhaps it already
// exists.
//
// If the file exists already, attempt to read it. Fail if that doesn't work.
// A should show to the user something that indicates that the file exists but is
// not already a database.
//
// If the file does not exist, create a new one. Again, show the user an error if
// some kind of error occurs.
// app.open_db(&path);
// s.change_view(HistoricalView::new().upcast());
// let s = s.clone();
/*
glib::spawn_future_local(async move {
s.app_tx
.send(AppInvocation::OpenDatabase(path))
.await
.unwrap();
});
*/
s.app_tx
.send_blocking(AppInvocation::OpenDatabase(path))
.unwrap();
})
})
.upcast()
} else {
HistoricalView::new().upcast()
};
s.change_view(initial_view);
s 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. // Switch views.
// //
// This function only replaces the old view with the one which matches the current view state. // 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 // It is responsible for ensuring that the new view goes into the layout in the correct
// position. // position.
fn change_view(&self, view: gtk::Widget) { fn swap_main(&self, view: View) {
let mut current_view = self.current_view.borrow_mut(); let mut current_widget = self.current_view.borrow_mut();
self.layout.remove(&*current_view); self.layout.remove(&*current_widget.widget());
*current_view = view; *current_widget = view;
self.layout.append(&*current_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()),
}
} }
} }
@ -354,10 +391,14 @@ fn main() {
}; };
let settings = gio::Settings::new(app_id); let settings = gio::Settings::new(app_id);
let app = App::new({
println!("database path: {}", settings.string("series-path")); let path = settings.string("series-path");
if path.is_empty() {
let app = App::new(settings); None
} else {
Some(PathBuf::from(path))
}
});
let adw_app = adw::Application::builder() let adw_app = adw::Application::builder()
.application_id(app_id) .application_id(app_id)
@ -373,17 +414,26 @@ fn main() {
let (gtk_tx, gtk_rx) = async_channel::unbounded::<AppResponse>(); let (gtk_tx, gtk_rx) = async_channel::unbounded::<AppResponse>();
let (app_tx, app_rx) = async_channel::unbounded::<AppInvocation>(); let (app_tx, app_rx) = async_channel::unbounded::<AppInvocation>();
AppWindow::new(adw_app, app_tx.clone()); let window = AppWindow::new(app_id, adw_app, app_tx.clone());
glib::spawn_future_local(async move { 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;
while let Ok(response) = gtk_rx.recv().await { while let Ok(response) = gtk_rx.recv().await {
println!("response received: {:?}", response); println!("response received: {:?}", response);
window.process_response(response);
} }
}); });
runtime.spawn(async move { runtime.spawn({
while let Ok(invocation) = app_rx.recv().await { let app = app.clone();
println!("Received an invocation: {:?}", invocation); async move {
while let Ok(invocation) = app_rx.recv().await {
let response = app.process_invocation(invocation).await;
let _ = gtk_tx.send(response).await;
}
} }
}); });
}); });