Compare commits

...

4 Commits

6 changed files with 154 additions and 47 deletions

View File

@ -3,9 +3,10 @@ pub mod ui;
mod view_models;
mod views;
use kifu_core::{Core, CoreRequest, CoreResponse};
use std::sync::Arc;
use tokio::{runtime::Runtime, spawn};
use async_std::task::yield_now;
use kifu_core::{Core, CoreRequest, CoreResponse, Observable};
use std::{rc::Rc, sync::Arc};
use tokio::runtime::Runtime;
#[derive(Clone)]
pub struct CoreApi {
@ -37,3 +38,48 @@ where
println!("[Trace: {}] {:?}", trace_name, end - start);
result
}
/// LocalObserver creates a task on the current thread which watches the specified observer for notifications and calls the handler function with each one.
///
/// The LocalObserver starts a task which listens for notifications during the constructor. When the observer goes out of scope, it will make a point of aborting the task. This combination means that anything which uses the observer can create it, hold on to a reference of it, and then drop it when done, and not have to do anything else with the observer object.
struct LocalObserver<T> {
join_handle: glib::JoinHandle<()>,
handler: Rc<dyn Fn(T)>,
}
impl<T: 'static> LocalObserver<T> {
/// Construct a new LocalObserver and start it running.
///
/// observable -- any object which emits events
/// handler -- a function which can process events
fn new(observable: &dyn Observable<T>, handler: impl Fn(T) + 'static) -> Self {
let listener = observable.subscribe();
let handler = Rc::new(handler);
let join_handle = glib::spawn_future_local({
let handler = handler.clone();
async move {
loop {
match listener.recv().await {
Ok(msg) => handler(msg),
Err(_) => {
// recv only fails if the channel has been closed and no other notifications are pending. This will break out of the loop and terminate the observer.
return;
}
}
yield_now().await;
}
}
});
Self {
join_handle,
handler,
}
}
}
impl<T> Drop for LocalObserver<T> {
fn drop(&mut self) {
// Abort the task when the observer goes out of scope.
self.join_handle.abort();
}
}

View File

@ -0,0 +1,38 @@
/*
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
This file is part of Kifu.
Kifu 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.
Kifu 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 Kifu. If not, see <https://www.gnu.org/licenses/>.
*/
use crate::LocalObserver;
use kifu_core::{Core, CoreNotification};
pub struct GameReviewViewModel {
core: Core,
notification_observer: LocalObserver<CoreNotification>,
widget: gtk::Box,
}
impl GameReviewViewModel {
fn new(core: Core) -> Self {
let notification_observer = LocalObserver::new(&core, |msg| {
println!("GameReviewViewModel called with message: {:?}", msg)
});
Self {
core,
notification_observer,
widget: gtk::Box::new(gtk::Orientation::Horizontal, 0),
}
}
}

View File

@ -14,13 +14,12 @@ General Public License for more details.
You should have received a copy of the GNU General Public License along with Kifu. If not, see <https://www.gnu.org/licenses/>.
*/
use async_std::{
channel::Receiver,
task::{spawn, yield_now, JoinHandle},
};
use async_std::{channel::Receiver, task::yield_now};
use kifu_core::{Color, Core, CoreNotification, Goban, Observable, Player};
use std::{cell::RefCell, rc::Rc, time::Duration};
use crate::LocalObserver;
pub struct GameState {
goban: Goban,
white_clock: Duration,
@ -40,12 +39,13 @@ struct GameViewModelPrivate {
/// The Game View Model manages the current state of the game. It shows the two player cards, the board, the current capture count, the current player, and it maintains the UI for the clock (bearing in mind that the real clock is managed in the core). This view model should only be created once the details of the game, whether a game in progress or a new game (this view model won't know the difference) is known.
pub struct GameViewModel {
core: Core,
notification_observer: LocalObserver<CoreNotification>,
widget: gtk::Box,
data: Rc<RefCell<GameViewModelPrivate>>,
}
impl GameViewModelPrivate {
fn handle(&mut self, message: CoreNotification) {}
fn handle(&mut self, _message: CoreNotification) {}
}
impl GameViewModel {
@ -56,31 +56,15 @@ impl GameViewModel {
state: game,
}));
let s = Self {
let notification_observer = LocalObserver::new(&core, |msg| {
println!("GameViewModelHandler called with message: {:?}", msg)
});
Self {
core,
notification_observer,
widget: gtk::Box::new(gtk::Orientation::Horizontal, 0),
data,
};
let notifications = s.core.subscribe();
let data = s.data.clone();
glib::spawn_future_local(Self::listen(notifications, data));
s
}
async fn listen(
notifications: Receiver<CoreNotification>,
data: Rc<RefCell<GameViewModelPrivate>>,
) {
loop {
match notifications.recv().await {
Ok(msg) => data.borrow_mut().handle(msg),
Err(err) => {
unimplemented!("Should display an error message in the UI: {}", err)
}
}
yield_now().await;
}
}
}

View File

@ -14,30 +14,25 @@ General Public License for more details.
You should have received a copy of the GNU General Public License along with Kifu. If not, see <https://www.gnu.org/licenses/>.
*/
use async_channel::Receiver;
use async_std::task::{spawn, spawn_blocking, yield_now, JoinHandle};
use crate::LocalObserver;
use kifu_core::{Core, CoreNotification};
/// DatabaseViewModel controls the view that the user sees when starting the application if the application has been configured and if there are no games in progress. It provides a window into the database, showing a list of recently recorded games (whether from this app or from a main database). It also provides the UI for starting a new game. This will render an empty database view if the user hasn't configured a database yet.
pub struct DatabaseViewModel {
/// Home controls the view that the user sees when starting the application if there are no games in progress. It provides a window into the database, showing a list of recently recorded games. It also provides the UI for starting a new game. This will render an empty database view if the user hasn't configured a database yet.
pub struct HomeViewModel {
core: Core,
notification_handler: JoinHandle<()>,
notification_observer: LocalObserver<CoreNotification>,
widget: gtk::Box,
}
impl DatabaseViewModel {
fn new(core: Core, notifications: Receiver<CoreNotification>) -> Self {
let handler = spawn(async move {
loop {
let message = notifications.recv().await;
println!("Message received in DatabaseViewModel: {:?}", message);
yield_now().await;
}
impl HomeViewModel {
fn new(core: Core) -> Self {
let notification_observer = LocalObserver::new(&core, |msg| {
println!("DatabaseViewModelHandler called with message: {:?}", msg)
});
Self {
core,
notification_handler: handler,
notification_observer,
widget: gtk::Box::new(gtk::Orientation::Horizontal, 0),
}
}

View File

@ -20,8 +20,14 @@ Every view model requires a reference to the app so that it can call functions o
The view model is primary over the view. It will construct the view, it can make major changes to the view or even swap for another related view. It must listen for all messages from the core, discarding those that aren't relevant to it. It will also convert requests from sync to async.
*/
mod database_view_model;
pub use database_view_model::DatabaseViewModel;
mod game_view_model;
pub use game_view_model::GameViewModel;
mod game_review_view_model;
pub use game_review_view_model::GameReviewViewModel;
mod home_view_model;
pub use home_view_model::HomeViewModel;
mod settings_view_model;
pub use settings_view_model::SettingsViewModel;

View File

@ -0,0 +1,38 @@
/*
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
This file is part of Kifu.
Kifu 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.
Kifu 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 Kifu. If not, see <https://www.gnu.org/licenses/>.
*/
use crate::LocalObserver;
use kifu_core::{Core, CoreNotification};
pub struct SettingsViewModel {
core: Core,
notification_observer: LocalObserver<CoreNotification>,
widget: gtk::Box,
}
impl SettingsViewModel {
fn new(core: Core) -> Self {
let notification_observer = LocalObserver::new(&core, |msg| {
println!("SettingsViewModel called with message: {:?}", msg)
});
Self {
core,
notification_observer,
widget: gtk::Box::new(gtk::Orientation::Horizontal, 0),
}
}
}