diff --git a/config/src/lib.rs b/config/src/lib.rs index 054d96a..4a24421 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -35,7 +35,7 @@ macro_rules! define_config { $($name($struct)),+ } - #[derive(Clone, Debug)] + #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct Config { values: std::collections::HashMap, } diff --git a/kifu/core/src/api.rs b/kifu/core/src/api.rs index 9c01419..405d63b 100644 --- a/kifu/core/src/api.rs +++ b/kifu/core/src/api.rs @@ -1,5 +1,22 @@ +/* +Copyright 2024, Savanni D'Gerinel + +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 . +*/ + use crate::{ database::Database, + library, settings, types::{AppState, Config, ConfigOption, GameState, LibraryPath, Player, Rank}, }; use async_std::{ @@ -18,8 +35,11 @@ pub trait Observable { fn subscribe(&self) -> Receiver; } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub enum CoreRequest { + Library(library::LibraryRequest), + Settings(settings::SettingsRequest), + /* ChangeSetting(ChangeSettingRequest), CreateGame(CreateGameRequest), Home, @@ -27,8 +47,10 @@ pub enum CoreRequest { PlayingField, PlayStone(PlayStoneRequest), StartGame, + */ } +/* #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum ChangeSettingRequest { LibraryPath(String), @@ -65,16 +87,25 @@ impl From for Player { } } } +*/ -/* #[derive(Clone, Debug, Serialize, Deserialize)] pub enum CoreResponse { - ConfigurationView(ConfigurationView), - HomeView(HomeView), - PlayingFieldView(PlayingFieldView), - UpdatedConfigurationView(ConfigurationView), + Library(library::LibraryResponse), + Settings(settings::SettingsResponse), +} + +impl From for CoreResponse { + fn from(r: library::LibraryResponse) -> Self { + Self::Library(r) + } +} + +impl From for CoreResponse { + fn from(r: settings::SettingsResponse) -> Self { + Self::Settings(r) + } } -*/ #[derive(Clone, Debug)] pub enum CoreNotification { @@ -92,10 +123,18 @@ pub struct Core { impl Core { pub fn new(config: Config) -> Self { + println!("config: {:?}", config); + + let library = if let Some(ref path) = config.get::() { + Some(Database::open_path(path.to_path_buf()).unwrap()) + } else { + None + }; + Self { config: Arc::new(RwLock::new(config)), // state, - library: Arc::new(RwLock::new(None)), + library: Arc::new(RwLock::new(library)), subscribers: Arc::new(RwLock::new(vec![])), } } @@ -112,17 +151,41 @@ impl Core { /// configuration is not a decision for the core library. pub async fn set_config(&self, config: Config) { *self.config.write().unwrap() = config.clone(); - let subscribers = self.subscribers.read().unwrap().clone(); - for subscriber in subscribers { - let subscriber = subscriber.clone(); - let _ = subscriber.send(CoreNotification::ConfigurationUpdated(config.clone())).await; + + // let db = library::read_library(self.config.read().unwrap().get::()).await; + let library_path = self.config.read().unwrap().get::(); + if let Some(ref path) = library_path { + self.load_library(path); } + + self.notify(CoreNotification::ConfigurationUpdated(config.clone())) + .await; + } + + fn load_library(&self, path: &LibraryPath) { + let db = Database::open_path(path.to_path_buf()).unwrap(); + *self.library.write().unwrap() = Some(db); } pub fn library<'a>(&'a self) -> RwLockReadGuard<'_, Option> { self.library.read().unwrap() } + pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse { + match request { + CoreRequest::Library(request) => library::handle(&self, request).await.into(), + CoreRequest::Settings(request) => settings::handle(&self, request).await.into(), + } + } + + pub async fn notify(&self, notification: CoreNotification) { + let subscribers = self.subscribers.read().unwrap().clone(); + for subscriber in subscribers { + let subscriber = subscriber.clone(); + let _ = subscriber.send(notification.clone()).await; + } + } + /* pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse { match request { diff --git a/kifu/core/src/database.rs b/kifu/core/src/database.rs index 1fbb9ca..796d0ca 100644 --- a/kifu/core/src/database.rs +++ b/kifu/core/src/database.rs @@ -43,10 +43,12 @@ impl Database { match parse_sgf(&buffer) { Ok(sgfs) => { for sgf in sgfs { - games.push(sgf); + if let Ok(sgf) = sgf { + games.push(sgf); + } } } - Err(err) => println!("Error parsing {:?}: {:?}", entry.path(), err), + Err(err) => println!("Error parsing {:?}", entry.path()), } } } diff --git a/kifu/core/src/lib.rs b/kifu/core/src/lib.rs index 8a7f40f..e4206dc 100644 --- a/kifu/core/src/lib.rs +++ b/kifu/core/src/lib.rs @@ -1,13 +1,32 @@ +/* +Copyright 2024, Savanni D'Gerinel + +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 . +*/ + extern crate config_derive; mod api; -pub use api::{Core, CoreNotification, Observable}; +pub use api::{Core, CoreNotification, CoreRequest, CoreResponse, Observable}; mod board; pub use board::*; mod database; +pub mod library; + mod types; pub use types::{BoardError, Color, Config, ConfigOption, LibraryPath, Player, Rank, Size}; +pub mod settings; diff --git a/kifu/core/src/library.rs b/kifu/core/src/library.rs new file mode 100644 index 0000000..ddeb4ec --- /dev/null +++ b/kifu/core/src/library.rs @@ -0,0 +1,48 @@ +/* +Copyright 2024, Savanni D'Gerinel + +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 . +*/ + +use crate::{Core, Config}; +use serde::{Deserialize, Serialize}; +use sgf::Game; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum LibraryRequest { + ListGames +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum LibraryResponse { + Games(Vec) +} + +async fn handle_list_games(model: &Core) -> LibraryResponse { + let library = model.library(); + match *library { + Some(ref library) => { + let info = library.all_games().map(|g| g.clone()).collect::>(); + LibraryResponse::Games(info) + } + None => LibraryResponse::Games(vec![]), + } +} + + +pub async fn handle(model: &Core, request: LibraryRequest) -> LibraryResponse { + match request { + LibraryRequest::ListGames => handle_list_games(model).await, + } +} + diff --git a/kifu/core/src/settings.rs b/kifu/core/src/settings.rs new file mode 100644 index 0000000..1bcbc95 --- /dev/null +++ b/kifu/core/src/settings.rs @@ -0,0 +1,37 @@ +/* +Copyright 2024, Savanni D'Gerinel + +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 . +*/ + +use crate::{types::LibraryPath, Core, Config}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum SettingsRequest { + Get, + Set(Config), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SettingsResponse(pub Config); + +pub async fn handle(model: &Core, request: SettingsRequest) -> SettingsResponse { + match request { + SettingsRequest::Get => SettingsResponse(model.get_config()), + SettingsRequest::Set(config) => { + model.set_config(config).await; + SettingsResponse(model.get_config()) + } + } +} diff --git a/kifu/core/src/types.rs b/kifu/core/src/types.rs index 7f39a8b..e29eddc 100644 --- a/kifu/core/src/types.rs +++ b/kifu/core/src/types.rs @@ -1,5 +1,4 @@ use crate::{ - api::PlayStoneRequest, board::{Coordinate, Goban}, database::Database, }; @@ -85,6 +84,7 @@ impl AppState { } } + /* pub fn place_stone(&mut self, req: PlayStoneRequest) { if let Some(ref mut game) = self.game { let _ = game.place_stone(Coordinate { @@ -93,6 +93,7 @@ impl AppState { }); } } + */ } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] diff --git a/kifu/gtk/src/app_window.rs b/kifu/gtk/src/app_window.rs index 020d72c..6ead01c 100644 --- a/kifu/gtk/src/app_window.rs +++ b/kifu/gtk/src/app_window.rs @@ -14,21 +14,19 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with Kifu. If not, see . */ +use crate::CoreApi; use adw::prelude::*; -/* -use gio::resources_lookup_data; -use glib::IsA; -use gtk::STYLE_PROVIDER_PRIORITY_USER; -*/ -use kifu_core::{Config, Core}; +use async_std::task::{block_on, spawn}; +use kifu_core::settings::SettingsResponse; +use kifu_core::CoreResponse; +use kifu_core::{settings::SettingsRequest, Config, CoreRequest}; use std::sync::{Arc, RwLock}; -use crate::{view_models::HomeViewModel, view_models::SettingsViewModel}; +use crate::views::{SettingsView, HomeView}; #[derive(Clone)] enum AppView { - Settings(SettingsViewModel), - Home(HomeViewModel), + Home, } // An application window should generally contain @@ -52,15 +50,15 @@ pub struct AppWindow { // Overlays are for transient content, such as about and settings, which can be accessed from // anywhere but shouldn't be part of the main application flow. panel_overlay: gtk::Overlay, - core: Core, + core: CoreApi, // Not liking this, but I have to keep track of the settings view model separately from // anything else. I'll have to look into this later. - settings_view_model: Arc>>, + settings_view_model: Arc>>, } impl AppWindow { - pub fn new(app: &adw::Application, core: Core) -> Self { + pub fn new(app: &adw::Application, core: CoreApi) -> Self { let window = Self::setup_window(app); let header = Self::setup_header(); let panel_overlay = Self::setup_panel_overlay(); @@ -87,22 +85,56 @@ impl AppWindow { } pub fn open_settings(&self) { - let view_model = SettingsViewModel::new(&self.window, self.core.clone(), { + // This should return instantly and allow the UI to continue being functional. However, + // some tests indicate that this may not actually be working correctly, and that a + // long-running background thread may delay things. + glib::spawn_future_local({ let s = self.clone(); - move || { - s.close_overlay(); + async move { + if let CoreResponse::Settings(SettingsResponse(settings)) = s + .core + .dispatch(CoreRequest::Settings(SettingsRequest::Get)) + .await + { + let view_model = SettingsView::new( + &s.window, + settings, + { + let s = s.clone(); + move |config| { + glib::spawn_future_local({ + let s = s.clone(); + async move { + s.core + .dispatch(CoreRequest::Settings(SettingsRequest::Set( + config, + ))) + .await + } + }); + s.close_overlay(); + } + }, + { + let s = s.clone(); + move || { + s.close_overlay(); + } + }, + ); + s.panel_overlay.add_overlay(&view_model); + *s.settings_view_model.write().unwrap() = Some(view_model); + } } }); - self.panel_overlay.add_overlay(&view_model.widget); - *self.settings_view_model.write().unwrap() = Some(view_model); } pub fn close_overlay(&self) { - let mut vm = self.settings_view_model.write().unwrap(); - match *vm { - Some(ref mut view_model) => { - self.panel_overlay.remove_overlay(&view_model.widget); - *vm = None; + let mut view = self.settings_view_model.write().unwrap(); + match *view { + Some(ref mut settings) => { + self.panel_overlay.remove_overlay(settings); + *view = None; } None => {} } @@ -140,31 +172,19 @@ impl AppWindow { gtk::Overlay::new() } - fn setup_content(core: Core) -> (adw::NavigationView, Vec) { + fn setup_content(core: CoreApi) -> (adw::NavigationView, Vec) { let stack = adw::NavigationView::new(); - let mut content = Vec::new(); + let content = Vec::new(); - let nothing_page = adw::StatusPage::builder().title("Nothing here").build(); + let home = HomeView::new(core.clone()); let _ = stack.push( &adw::NavigationPage::builder() .can_pop(false) .title("Kifu") - .child(¬hing_page) + .child(&home) .build(), ); - content.push(AppView::Home(HomeViewModel::new(core.clone()))); - - /* - match *core.library() { - Some(_) => { - } - None => { - let settings_vm = SettingsViewModel::new(core.clone()); - let _ = stack.push(&adw::NavigationPage::new(&settings_vm.widget, "Settings")); - content.push(AppView::Settings(settings_vm)); - } - } - */ + // content.push(AppView::Home(HomeViewModel::new(core))); (stack, content) } diff --git a/kifu/gtk/src/ui/board.rs b/kifu/gtk/src/components/board.rs similarity index 100% rename from kifu/gtk/src/ui/board.rs rename to kifu/gtk/src/components/board.rs diff --git a/kifu/gtk/src/ui/chat.rs b/kifu/gtk/src/components/chat.rs similarity index 100% rename from kifu/gtk/src/ui/chat.rs rename to kifu/gtk/src/components/chat.rs diff --git a/kifu/gtk/src/ui/config.rs b/kifu/gtk/src/components/config.rs similarity index 100% rename from kifu/gtk/src/ui/config.rs rename to kifu/gtk/src/components/config.rs diff --git a/kifu/gtk/src/ui/game_preview.rs b/kifu/gtk/src/components/game_preview.rs similarity index 100% rename from kifu/gtk/src/ui/game_preview.rs rename to kifu/gtk/src/components/game_preview.rs diff --git a/kifu/gtk/src/ui/home.rs b/kifu/gtk/src/components/home.rs similarity index 100% rename from kifu/gtk/src/ui/home.rs rename to kifu/gtk/src/components/home.rs diff --git a/kifu/gtk/src/ui/library.rs b/kifu/gtk/src/components/library.rs similarity index 62% rename from kifu/gtk/src/ui/library.rs rename to kifu/gtk/src/components/library.rs index 46603ad..0cdec79 100644 --- a/kifu/gtk/src/ui/library.rs +++ b/kifu/gtk/src/components/library.rs @@ -1,12 +1,13 @@ use adw::{prelude::*, subclass::prelude::*}; use glib::Object; use gtk::glib; -use kifu_core::ui::GamePreviewElement; +// use kifu_core::ui::GamePreviewElement; +use sgf::Game; use std::{cell::RefCell, rc::Rc}; #[derive(Default)] pub struct GameObjectPrivate { - game: Rc>>, + game: Rc>>, } #[glib::object_subclass] @@ -22,13 +23,13 @@ glib::wrapper! { } impl GameObject { - pub fn new(game: GamePreviewElement) -> Self { + pub fn new(game: Game) -> Self { let s: Self = Object::builder().build(); *s.imp().game.borrow_mut() = Some(game); s } - pub fn game(&self) -> Option { + pub fn game(&self) -> Option { self.imp().game.borrow().clone() } } @@ -77,11 +78,14 @@ impl Default for LibraryPrivate { */ let selection_model = gtk::NoSelection::new(Some(model.clone())); - let list_view = gtk::ColumnView::builder().model(&selection_model).build(); + let list_view = gtk::ColumnView::builder() + .model(&selection_model) + .hexpand(true) + .build(); fn make_factory(bind: F) -> gtk::SignalListItemFactory where - F: Fn(GamePreviewElement) -> String + 'static, + F: Fn(Game) -> String + 'static, { let factory = gtk::SignalListItemFactory::new(); factory.connect_setup(|_, list_item| { @@ -106,25 +110,61 @@ impl Default for LibraryPrivate { factory } - list_view.append_column(>k::ColumnViewColumn::new( - Some("date"), - Some(make_factory(|g| g.date)), - )); - list_view.append_column(>k::ColumnViewColumn::new( - Some("title"), - Some(make_factory(|g| g.name)), - )); - list_view.append_column(>k::ColumnViewColumn::new( - Some("black"), - Some(make_factory(|g| g.black_player)), - )); - list_view.append_column(>k::ColumnViewColumn::new( - Some("white"), - Some(make_factory(|g| g.white_player)), - )); + list_view.append_column( + >k::ColumnViewColumn::builder() + .title("date") + .factory(&make_factory(|g| { + g.dates + .iter() + .map(|date| { + format!("{}", date) + /* + let l = locale!("en-US").into(); + let options = length::Bag::from_date_style(length::Date::Medium); + let date = Date::try_new_iso_date(date. + let dtfmt = + DateFormatter::try_new_with_length(&l, options).unwrap(); + dtfmt.format(date).unwrap() + */ + }) + .collect::>() + .join(", ") + })) + .expand(true) + .build(), + ); + list_view.append_column( + >k::ColumnViewColumn::builder() + .title("game") + .factory(&make_factory(|g| { + g.game_name.unwrap_or("Unnamed".to_owned()) + })) + .expand(true) + .build(), + ); + list_view.append_column( + >k::ColumnViewColumn::builder() + .title("black") + .factory(&make_factory(|g| { + g.black_player.name.unwrap_or("Black".to_owned()) + })) + .expand(true) + .build(), + ); + list_view.append_column( + >k::ColumnViewColumn::builder() + .title("white") + .factory(&make_factory(|g| { + g.white_player.name.unwrap_or("White".to_owned()) + })) + .expand(true) + .build(), + ); list_view.append_column(>k::ColumnViewColumn::new( Some("result"), - Some(make_factory(|g| g.result)), + Some(make_factory(|g| { + g.result.map(|d| format!("{}", d)).unwrap_or("".to_owned()) + })), )); Self { model, list_view } @@ -156,7 +196,7 @@ impl Default for Library { } impl Library { - pub fn set_games(&self, games: Vec) { + pub fn set_games(&self, games: Vec) { let games = games .into_iter() .map(GameObject::new) diff --git a/kifu/gtk/src/ui/mod.rs b/kifu/gtk/src/components/mod.rs similarity index 90% rename from kifu/gtk/src/ui/mod.rs rename to kifu/gtk/src/components/mod.rs index 43718f0..d673a45 100644 --- a/kifu/gtk/src/ui/mod.rs +++ b/kifu/gtk/src/components/mod.rs @@ -7,8 +7,8 @@ // mod game_preview; // pub use game_preview::GamePreview; -// mod library; -// pub use library::Library; +mod library; +pub use library::Library; // mod player_card; // pub use player_card::PlayerCard; diff --git a/kifu/gtk/src/ui/player_card.rs b/kifu/gtk/src/components/player_card.rs similarity index 100% rename from kifu/gtk/src/ui/player_card.rs rename to kifu/gtk/src/components/player_card.rs diff --git a/kifu/gtk/src/ui/playing_field.rs b/kifu/gtk/src/components/playing_field.rs similarity index 100% rename from kifu/gtk/src/ui/playing_field.rs rename to kifu/gtk/src/components/playing_field.rs diff --git a/kifu/gtk/src/lib.rs b/kifu/gtk/src/lib.rs index ca2ab98..c6c66f5 100644 --- a/kifu/gtk/src/lib.rs +++ b/kifu/gtk/src/lib.rs @@ -14,7 +14,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with Kifu. If not, see . */ -pub mod ui; +pub mod components; mod app_window; pub use app_window::AppWindow; @@ -22,31 +22,20 @@ pub use app_window::AppWindow; mod view_models; mod views; -use async_std::task::yield_now; -use kifu_core::{Core, Observable}; +use async_std::task::{spawn, yield_now}; +use kifu_core::{Core, Observable, CoreRequest, CoreResponse}; use std::{rc::Rc, sync::Arc}; use tokio::runtime::Runtime; #[derive(Clone)] pub struct CoreApi { - pub rt: Arc, pub core: Core, } impl CoreApi { - /* - pub fn dispatch(&self, request: CoreRequest) { - /* - spawn({ - /* - let gtk_tx = self.gtk_tx.clone(); - let core = self.core.clone(); - async move { gtk_tx.send(core.dispatch(request).await) } - */ - }); - */ + pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse { + self.core.dispatch(request).await } -*/ } pub fn perftrace(trace_name: &str, f: F) -> A diff --git a/kifu/gtk/src/main.rs b/kifu/gtk/src/main.rs index 0eaf729..b059371 100644 --- a/kifu/gtk/src/main.rs +++ b/kifu/gtk/src/main.rs @@ -90,7 +90,8 @@ fn setup_app_configuration_action(app: &adw::Application, app_window: AppWindow) println!("setup_app_configuration_action"); let action = ActionEntry::builder("show_settings") .activate(move |_app: &adw::Application, _, _| { - app_window.open_settings(); + let app_window = app_window.clone(); + app_window.open_settings() }) .build(); app.add_action_entries([action]); @@ -109,28 +110,7 @@ fn main() { let config = load_config(&app_id); - let runtime = Arc::new( - tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap(), - ); - - /* - let config_path = std::env::var("CONFIG") - .map(std::path::PathBuf::from) - .or({ - std::env::var("HOME").map(|base| { - let mut config_path = std::path::PathBuf::from(base); - config_path.push(".config"); - config_path.push("kifu"); - config_path - }) - }) - .expect("no config path could be found"); - */ - - let core = Core::new(config); + let core = Core::new(config.clone()); spawn({ let notifier = core.subscribe(); @@ -138,70 +118,22 @@ fn main() { handler(notifier, app_id) }); - /* - let core_handle = runtime.spawn({ - let core = core.clone(); - async move { - core.run().await; - } - }); - */ - let app = adw::Application::builder() .application_id("com.luminescent-dreams.kifu-gtk") .resource_base_path("/com/luminescent-dreams/kifu-gtk") .build(); app.connect_activate({ - let runtime = runtime.clone(); move |app| { - let mut app_window = AppWindow::new(app, core.clone()); - - match *core.library() { - Some(_) => {} - None => app_window.open_settings(), - } + let core_api = CoreApi { core: core.clone() }; + let app_window = AppWindow::new(app, core_api); setup_app_configuration_action(app, app_window.clone()); - /* - let api = CoreApi { - rt: runtime.clone(), - core: core.clone(), - }; - */ - - /* - let action_config = gio::SimpleAction::new("show-config", None); - action_config.connect_activate({ - let api = api.clone(); - move |_, _| { - api.dispatch(CoreRequest::OpenConfiguration); - } - }); - app.add_action(&action_config); - */ - app_window.window.present(); - - /* - gtk_rx.attach(None, { - let api = api.clone(); - move |message| { - perftrace("handle_response", || { - handle_response(api.clone(), &app_window, message) - }); - glib::ControlFlow::Continue - } - }); - */ - - // api.dispatch(CoreRequest::Home); } }); println!("running the gtk loop"); app.run(); - - /* let _ = runtime.block_on(core_handle); */ } diff --git a/kifu/gtk/src/view_models/home_view_model.rs b/kifu/gtk/src/view_models/home_view_model.rs index 5af979a..0ce1e7e 100644 --- a/kifu/gtk/src/view_models/home_view_model.rs +++ b/kifu/gtk/src/view_models/home_view_model.rs @@ -16,18 +16,22 @@ You should have received a copy of the GNU General Public License along with Kif use crate::LocalObserver; use kifu_core::{Core, CoreNotification}; +use crate::CoreApi; use std::sync::Arc; /// 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. #[derive(Clone)] pub struct HomeViewModel { - core: Core, + /* + core: CoreApi, notification_observer: Arc>, widget: gtk::Box, + */ } impl HomeViewModel { - pub fn new(core: Core) -> Self { + pub fn new(core: CoreApi) -> Self { + /* let notification_observer = LocalObserver::new(&core, |msg| { println!("HomeViewModel handler called with message: {:?}", msg) }); @@ -37,6 +41,8 @@ impl HomeViewModel { notification_observer: Arc::new(notification_observer), widget: gtk::Box::new(gtk::Orientation::Horizontal, 0), } + */ + Self {} } /// Create a new game with the given parameters. diff --git a/kifu/gtk/src/view_models/mod.rs b/kifu/gtk/src/view_models/mod.rs index c69dcf9..a819b9b 100644 --- a/kifu/gtk/src/view_models/mod.rs +++ b/kifu/gtk/src/view_models/mod.rs @@ -28,6 +28,3 @@ 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; diff --git a/kifu/gtk/src/view_models/settings_view_model.rs b/kifu/gtk/src/view_models/settings_view_model.rs deleted file mode 100644 index e505a11..0000000 --- a/kifu/gtk/src/view_models/settings_view_model.rs +++ /dev/null @@ -1,73 +0,0 @@ -/* -Copyright 2024, Savanni D'Gerinel - -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 . -*/ - -use crate::{views, views::SettingsView, LocalObserver}; -use async_std::task::spawn; -use gtk::prelude::*; -use kifu_core::{Config, Core, CoreNotification}; -use std::{sync::Arc, rc::Rc}; - -/// SettingsViewModel -/// -/// Listens for messages from the core, and serves as intermediary between the Settings UI and the -/// core. Because it needs to respond to events from the core, it owns the widget, which allows it -/// to tell the widget to update after certain events. -#[derive(Clone)] -pub struct SettingsViewModel { - core: Core, - // Technically, Settings doesn't care about any events from Core. We will keep this around for - // now as reference, until something which does care shows up. - notification_observer: Arc>, - pub widget: views::SettingsView, -} - -impl SettingsViewModel { - pub fn new(parent: &impl IsA, core: Core, on_close: impl Fn() + 'static) -> Self { - let on_close = Arc::new(on_close); - - let notification_observer = LocalObserver::new(&core, |msg| { - println!("SettingsViewModel called with message: {:?}", msg) - }); - - let config = core.get_config(); - - let widget = SettingsView::new( - parent, - config, - { - let core = core.clone(); - let on_close = on_close.clone(); - move |new_config| { - spawn({ - let core = core.clone(); - on_close(); - async move { - println!("running set_config in the background"); - core.set_config(new_config).await; - } - }); - } - }, - move || on_close(), - ); - - Self { - core: core.clone(), - notification_observer: Arc::new(notification_observer), - widget, - } - } -} diff --git a/kifu/gtk/src/views/home.rs b/kifu/gtk/src/views/home.rs new file mode 100644 index 0000000..b03dca2 --- /dev/null +++ b/kifu/gtk/src/views/home.rs @@ -0,0 +1,208 @@ +/* +Copyright 2024, Savanni D'Gerinel + +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 . +*/ + +use crate::{components::Library, CoreApi}; +use glib::Object; +use gtk::{glib, prelude::*, subclass::prelude::*}; +use kifu_core::{ + library::{LibraryRequest, LibraryResponse}, + CoreRequest, CoreResponse, +}; +use std::{cell::RefCell, rc::Rc}; + +/* +struct PlayerDataEntryPrivate { + name: gtk::Text, + rank: gtk::DropDown, +} + +impl Default for PlayerDataEntryPrivate { + fn default() -> Self { + let rank = gtk::DropDown::builder().build(); + Self { + name: gtk::Text::builder().build(), + rank, + } + } +} + +#[glib::object_subclass] +impl ObjectSubclass for PlayerDataEntryPrivate { + const NAME: &'static str = "PlayerDataEntry"; + type Type = PlayerDataEntry; + type ParentType = gtk::Box; +} + +impl ObjectImpl for PlayerDataEntryPrivate {} +impl WidgetImpl for PlayerDataEntryPrivate {} +impl BoxImpl for PlayerDataEntryPrivate {} + +glib::wrapper! { + struct PlayerDataEntry(ObjectSubclass) @extends gtk::Box, gtk::Widget; +} + +impl PlayerDataEntry { + pub fn new(element: PlayerElement) -> PlayerDataEntry { + let s: Self = Object::builder().build(); + + let rank_model = gio::ListStore::new::(); + s.imp().rank.set_model(Some(&rank_model)); + + match element { + PlayerElement::Hotseat(player) => { + if let Some(placeholder) = player.placeholder { + s.imp().name.set_placeholder_text(Some(&placeholder)); + } + player.ranks.iter().for_each(|rank| rank_model.append(>k::StringObject::new(rank))); + } + // PlayerElement::Remote(_) => s.imp().placeholder.set_text("remote player"), + // PlayerElement::Bot(_) => s.imp().placeholder.set_text("bot player"), + } + + s.append(&s.imp().name); + s.append(&s.imp().rank); + + s + } + + pub fn text(&self) -> String { + let name = self.imp().name.buffer().text().to_string(); + if name.is_empty() { + self.imp() + .name + .placeholder_text() + .map(|s| s.to_string()) + .unwrap_or("".to_owned()) + } else { + name + } + } + + pub fn rank(&self) -> Option { + self.imp().rank.selected_item().and_then(|obj| { + let str_obj = obj.downcast::().ok()?; + Some(str_obj.string().clone().to_string()) + }) + } +} +*/ + +pub struct HomePrivate { + // black_player: Rc>>, + // white_player: Rc>>, +} + +impl Default for HomePrivate { + fn default() -> Self { + Self { + // black_player: Rc::new(RefCell::new(None)), + // white_player: Rc::new(RefCell::new(None)), + } + } +} + +#[glib::object_subclass] +impl ObjectSubclass for HomePrivate { + const NAME: &'static str = "Home"; + type Type = HomeView; + type ParentType = gtk::Box; +} + +impl ObjectImpl for HomePrivate {} +impl WidgetImpl for HomePrivate {} +impl BoxImpl for HomePrivate {} + +glib::wrapper! { + pub struct HomeView(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; +} + +impl HomeView { + pub fn new(api: CoreApi) -> Self { + let s: Self = Object::builder().build(); + s.set_spacing(4); + s.set_homogeneous(false); + s.set_orientation(gtk::Orientation::Vertical); + + /* + let players = gtk::Box::builder() + .spacing(4) + .orientation(gtk::Orientation::Horizontal) + .build(); + s.append(&players); + + let black_player = PlayerDataEntry::new(view.black_player); + players.append(&black_player); + *s.imp().black_player.borrow_mut() = Some(black_player.clone()); + let white_player = PlayerDataEntry::new(view.white_player); + players.append(&white_player); + *s.imp().white_player.borrow_mut() = Some(white_player.clone()); + + let new_game_button = gtk::Button::builder() + .css_classes(vec!["suggested-action"]) + .label(&view.start_game.label) + .build(); + s.append(&new_game_button); + */ + + let library = Library::default(); + let library_view = gtk::ScrolledWindow::builder() + .hscrollbar_policy(gtk::PolicyType::Never) + .min_content_width(360) + .vexpand(true) + .hexpand(true) + .child(&library) + .build(); + s.append(&library_view); + + glib::spawn_future_local({ + let library = library.clone(); + let api = api.clone(); + async move { + if let CoreResponse::Library(LibraryResponse::Games(games)) = api + .dispatch(CoreRequest::Library(LibraryRequest::ListGames)) + .await + { + library.set_games(games); + } + } + }); + + /* + new_game_button.connect_clicked({ + move |_| { + let black_player = black_player.clone(); + let white_player = white_player.clone(); + + api.dispatch(CoreRequest::CreateGame(CreateGameRequest { + black_player: player_info(black_player.clone()), + white_player: player_info(white_player.clone()), + })); + } + }); + */ + + s + } +} + +/* +fn player_info(player: PlayerDataEntry) -> PlayerInfoRequest { + PlayerInfoRequest::Hotseat(HotseatPlayerRequest { + name: player.text(), + rank: player.rank(), + }) +} +*/ diff --git a/kifu/gtk/src/views/mod.rs b/kifu/gtk/src/views/mod.rs index e95b387..414675c 100644 --- a/kifu/gtk/src/views/mod.rs +++ b/kifu/gtk/src/views/mod.rs @@ -1,2 +1,5 @@ +mod home; +pub use home::HomeView; + mod settings; pub use settings::SettingsView; diff --git a/sgf/src/game.rs b/sgf/src/game.rs index e1d967b..dd8ffc2 100644 --- a/sgf/src/game.rs +++ b/sgf/src/game.rs @@ -2,6 +2,7 @@ use crate::{ parser::{self, Annotation, Evaluation, Move, SetupInstr, Size, UnknownProperty}, Color, Date, GameResult, GameType, }; +use serde::{Deserialize, Serialize}; use std::{collections::HashSet, time::Duration}; use uuid::Uuid; @@ -36,7 +37,7 @@ pub enum GameNodeError { ConflictingPosition, } -#[derive(Clone, Debug, PartialEq, Default)] +#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize)] pub struct Player { pub name: Option, pub rank: Option, @@ -51,7 +52,7 @@ pub struct Player { /// syntax issues, the result of the Game is to have a fully-understood game. However, this doesn't /// (yet?) go quite to the level of apply the game type (i.e., this is Go, Chess, Yinsh, or /// whatever). -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct Game { pub game_type: GameType, @@ -216,7 +217,7 @@ impl TryFrom<&parser::Tree> for Game { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub enum GameNode { MoveNode(MoveNode), SetupNode(SetupNode), @@ -317,7 +318,7 @@ impl TryFrom<&parser::Node> for GameNode { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct MoveNode { id: Uuid, color: Color, @@ -426,7 +427,7 @@ impl TryFrom<&parser::Node> for MoveNode { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct SetupNode { id: Uuid, diff --git a/sgf/src/parser.rs b/sgf/src/parser.rs index ce128a1..e60007f 100644 --- a/sgf/src/parser.rs +++ b/sgf/src/parser.rs @@ -9,6 +9,7 @@ use nom::{ multi::{many0, many1, separated_list1}, IResult, Parser, }; +use serde::{Deserialize, Serialize}; use std::{num::ParseIntError, time::Duration}; impl From for Error { @@ -29,7 +30,7 @@ impl From for ParseSizeError { } } -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)] pub enum Annotation { BadMove, DoubtfulMove, @@ -37,7 +38,7 @@ pub enum Annotation { Tesuji, } -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)] pub enum Evaluation { Even, GoodForBlack, @@ -147,7 +148,7 @@ impl ToString for GameType { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct Size { pub width: i32, pub height: i32, @@ -200,7 +201,7 @@ pub struct Node { pub next: Vec, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub enum SetupInstr { Piece((Color, String)), Clear(String), @@ -288,7 +289,7 @@ impl ToString for Node { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub enum Move { Move(String), Pass, diff --git a/sgf/src/types.rs b/sgf/src/types.rs index cdd0730..3cf3d13 100644 --- a/sgf/src/types.rs +++ b/sgf/src/types.rs @@ -1,6 +1,9 @@ use thiserror::Error; -#[derive(Clone, Debug, PartialEq)] +use serde::{Deserialize, Serialize}; +use std::fmt; + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub enum GameType { Go, Othello, @@ -93,7 +96,7 @@ impl From> for ParseError { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub enum Color { Black, White, @@ -109,7 +112,7 @@ impl Color { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum GameResult { Draw, Black(Win), @@ -118,6 +121,18 @@ pub enum GameResult { Unknown(String), } +impl fmt::Display for GameResult { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + GameResult::Draw => write!(f, "draw"), + GameResult::Black(_) => write!(f, "B"), + GameResult::White(_) => write!(f, "W"), + GameResult::Void => write!(f, "void"), + GameResult::Unknown(s) => write!(f, "{}", s), + } + } +} + impl TryFrom<&str> for GameResult { type Error = String; fn try_from(s: &str) -> Result { @@ -145,7 +160,7 @@ impl TryFrom<&str> for GameResult { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum Win { Score(f32), Resignation,