diff --git a/kifu/core/src/api.rs b/kifu/core/src/api.rs index 5871d7e..e87d7c7 100644 --- a/kifu/core/src/api.rs +++ b/kifu/core/src/api.rs @@ -16,8 +16,8 @@ You should have received a copy of the GNU General Public License along with Kif use crate::{ database::Database, + library, settings, types::{AppState, Config, ConfigOption, GameState, LibraryPath, Player, Rank}, - settings, }; use async_std::{ channel::{Receiver, Sender}, @@ -37,7 +37,8 @@ pub trait Observable { #[derive(Clone, Debug, Serialize, Deserialize)] pub enum CoreRequest { - Settings(settings::SettingsRequest) + Library(library::LibraryRequest), + Settings(settings::SettingsRequest), /* ChangeSetting(ChangeSettingRequest), CreateGame(CreateGameRequest), @@ -49,6 +50,7 @@ pub enum CoreRequest { */ } +/* #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum ChangeSettingRequest { LibraryPath(String), @@ -85,16 +87,18 @@ impl From for Player { } } } +*/ #[derive(Clone, Debug, Serialize, Deserialize)] pub enum CoreResponse { - Settings(settings::SettingsResponse) -/* - 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 { @@ -120,10 +124,18 @@ pub struct Core { impl Core { pub fn new(config: Config) -> Self { println!("config: {:?}", config); + + let library = if let Some(ref path) = config.get::() { + println!("loading initial library"); + 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![])), } } @@ -140,11 +152,20 @@ 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> { @@ -153,10 +174,19 @@ impl Core { 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/lib.rs b/kifu/core/src/lib.rs index 58c17c9..e4206dc 100644 --- a/kifu/core/src/lib.rs +++ b/kifu/core/src/lib.rs @@ -1,3 +1,19 @@ +/* +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; @@ -8,6 +24,8 @@ pub use board::*; mod database; +pub mod library; + mod types; pub use types::{BoardError, Color, Config, ConfigOption, LibraryPath, Player, Rank, Size}; diff --git a/kifu/core/src/library.rs b/kifu/core/src/library.rs new file mode 100644 index 0000000..3bf88f4 --- /dev/null +++ b/kifu/core/src/library.rs @@ -0,0 +1,50 @@ +/* +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::GameInfo; + +#[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 { + println!("handle_list_games"); + let library = model.library(); + println!("library: {:?}", *library); + match *library { + Some(ref library) => { + let info = library.all_games().map(|g| g.info.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/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 322d585..6ead01c 100644 --- a/kifu/gtk/src/app_window.rs +++ b/kifu/gtk/src/app_window.rs @@ -22,12 +22,11 @@ use kifu_core::CoreResponse; use kifu_core::{settings::SettingsRequest, Config, CoreRequest}; use std::sync::{Arc, RwLock}; -use crate::view_models::HomeViewModel; -use crate::views::SettingsView; +use crate::views::{SettingsView, HomeView}; #[derive(Clone)] enum AppView { - Home(HomeViewModel), + Home, } // An application window should generally contain @@ -92,36 +91,40 @@ impl AppWindow { glib::spawn_future_local({ let s = self.clone(); async move { - let CoreResponse::Settings(SettingsResponse(settings)) = s + 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); + .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); + } } }); } @@ -171,17 +174,17 @@ impl AppWindow { 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))); + // 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..1ec5aef 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::GameInfo; 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: GameInfo) -> 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(GameInfo) -> 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.date + .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.unwrap_or("Black".to_owned()) + })) + .expand(true) + .build(), + ); + list_view.append_column( + >k::ColumnViewColumn::builder() + .title("white") + .factory(&make_factory(|g| { + g.white_player.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 0034608..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; 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/types.rs b/sgf/src/types.rs index 605f12a..29499d6 100644 --- a/sgf/src/types.rs +++ b/sgf/src/types.rs @@ -1,4 +1,6 @@ use crate::date::Date; +use serde::{Deserialize, Serialize}; +use std::fmt; use thiserror::Error; @@ -9,7 +11,7 @@ pub struct Game { pub info: GameInfo, } -#[derive(Debug, Default)] +#[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct GameInfo { pub black_player: Option, pub black_rank: Option, @@ -129,7 +131,7 @@ impl Color { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum GameResult { Draw, Black(Win), @@ -138,6 +140,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 { @@ -165,7 +179,7 @@ impl TryFrom<&str> for GameResult { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum Win { Score(f32), Resignation,