diff --git a/Cargo.lock b/Cargo.lock index abd507e..de43e70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -910,7 +910,7 @@ dependencies = [ "gdk4", "gio", "glib", - "glib-build-tools", + "glib-build-tools 0.16.3", "gtk4", "libadwaita", "serde", @@ -1348,7 +1348,7 @@ dependencies = [ "gtk4", "image", "kifu-core", - "screenplay", + "sgf", "tokio", ] diff --git a/config/src/lib.rs b/config/src/lib.rs index 1c2b36b..054d96a 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -30,6 +30,7 @@ macro_rules! define_config { } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] + #[serde(untagged)] pub enum ConfigOption { $($name($struct)),+ } diff --git a/kifu/core/src/api.rs b/kifu/core/src/api.rs index 38c4d84..fead3e7 100644 --- a/kifu/core/src/api.rs +++ b/kifu/core/src/api.rs @@ -69,15 +69,11 @@ pub struct CoreApp { impl CoreApp { pub fn new(config_path: std::path::PathBuf) -> Self { - println!("config_path: {:?}", config_path); let config = Config::from_path(config_path).expect("configuration to open"); let db_path: DatabasePath = config.get().unwrap(); let state = Arc::new(RwLock::new(AppState::new(db_path))); - println!("config: {:?}", config); - println!("games database: {:?}", state.read().unwrap().database.len()); - Self { config, state } } diff --git a/kifu/core/src/ui/elements/game_preview.rs b/kifu/core/src/ui/elements/game_preview.rs index 1624fb9..afc9e2b 100644 --- a/kifu/core/src/ui/elements/game_preview.rs +++ b/kifu/core/src/ui/elements/game_preview.rs @@ -1,36 +1,52 @@ use serde::{Deserialize, Serialize}; -use sgf::{ - go::{Game, Rank}, - Date, -}; +use sgf::go::Game; use typeshare::typeshare; #[derive(Clone, Debug, Deserialize, Serialize)] #[typeshare] pub struct GamePreviewElement { - pub date: Vec, + pub date: Vec, + pub name: String, pub black_player: String, - pub black_rank: Option, pub white_player: String, - pub white_rank: Option, } impl GamePreviewElement { pub fn new(game: &Game) -> GamePreviewElement { + let black_player = match game.info.black_player { + Some(ref black_player) => black_player.clone(), + None => "unknown".to_owned(), + }; + let white_player = match game.info.white_player { + Some(ref white_player) => white_player.clone(), + None => "unknown".to_owned(), + }; + + let black_player = match game.info.black_rank { + Some(rank) => format!("{} ({})", black_player, rank.to_string()), + None => black_player, + }; + + let white_player = match game.info.white_rank { + Some(rank) => format!("{} ({})", white_player, rank.to_string()), + None => white_player, + }; + + let name = match game.info.game_name { + Some(ref name) => name.clone(), + None => format!("{} vs. {}", black_player, white_player), + }; + GamePreviewElement { - date: game.info.date.clone(), - black_player: game + date: game .info - .black_player - .clone() - .unwrap_or("black_player".to_owned()), - black_rank: game.info.black_rank.clone(), - white_player: game - .info - .white_player - .clone() - .unwrap_or("white_player".to_owned()), - white_rank: game.info.white_rank.clone(), + .date + .iter() + .map(|dt| dt.to_string()) + .collect::>(), + name, + black_player, + white_player, } } } diff --git a/kifu/core/src/ui/launch_screen.rs b/kifu/core/src/ui/launch_screen.rs deleted file mode 100644 index 3696423..0000000 --- a/kifu/core/src/ui/launch_screen.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::{ - ui::types; -}; -use serde::{Deserialize, Serialize}; -use typeshare::typeshare; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum LaunchScreenView { - CreateGame(CreateGameView) -} - -// This will be called when the Kifu application starts. -pub fn launch_screen() -> LaunchScreenView { -} diff --git a/kifu/core/src/ui/mod.rs b/kifu/core/src/ui/mod.rs index e9b5b7d..7233ec5 100644 --- a/kifu/core/src/ui/mod.rs +++ b/kifu/core/src/ui/mod.rs @@ -4,9 +4,6 @@ pub use elements::{action::Action, game_preview::GamePreviewElement, menu::Menu} mod playing_field; pub use playing_field::{playing_field, PlayingFieldView}; -// mod launch_screen; -// pub use launch_screen::{launch_screen, LaunchScreenView}; - mod home; pub use home::{home, HomeView, HotseatPlayerElement, PlayerElement}; diff --git a/kifu/gtk/Cargo.toml b/kifu/gtk/Cargo.toml index fe09ef7..21918ab 100644 --- a/kifu/gtk/Cargo.toml +++ b/kifu/gtk/Cargo.toml @@ -12,11 +12,12 @@ screenplay = [] cairo-rs = { version = "0.17" } gio = { version = "0.17" } glib = { version = "0.17" } -gtk = { version = "0.6", package = "gtk4" } +gtk = { version = "0.6", package = "gtk4", features = ["v4_8"] } image = { version = "0.24" } kifu-core = { path = "../core" } tokio = { version = "1.26", features = [ "full" ] } -screenplay = { path = "../../screenplay" } +# screenplay = { path = "../../screenplay" } +sgf = { path = "../../sgf" } [build-dependencies] glib-build-tools = "0.17" diff --git a/kifu/gtk/config b/kifu/gtk/config index 6ad4a86..a387288 100644 --- a/kifu/gtk/config +++ b/kifu/gtk/config @@ -1 +1,7 @@ -{"Me":{"name":"Savanni","rank":{"Kyu":10}},"DatabasePath":"../core/fixtures/five_games"} +{ + "Me":{ + "name":"Savanni", + "rank":{"Kyu":10} + }, + "DatabasePath": "kifu/core/fixtures/five_games" +} diff --git a/kifu/gtk/src/main.rs b/kifu/gtk/src/main.rs index 95cda24..499acf0 100644 --- a/kifu/gtk/src/main.rs +++ b/kifu/gtk/src/main.rs @@ -13,8 +13,8 @@ fn handle_response(api: CoreApi, window: gtk::ApplicationWindow, message: CoreRe CoreResponse::HomeView(view) => perftrace("HomeView", || { let api = api.clone(); - let new_game = Home::new(api, view); - window.set_child(Some(&new_game)); + let home = Home::new(api, view); + window.set_child(Some(&home)); }), CoreResponse::PlayingFieldView(view) => perftrace("PlayingFieldView", || { let api = api.clone(); diff --git a/kifu/gtk/src/ui/game_preview.rs b/kifu/gtk/src/ui/game_preview.rs index 64e2d92..ed0d131 100644 --- a/kifu/gtk/src/ui/game_preview.rs +++ b/kifu/gtk/src/ui/game_preview.rs @@ -3,7 +3,12 @@ use gtk::{glib, prelude::*, subclass::prelude::*}; use kifu_core::ui::GamePreviewElement; #[derive(Default)] -pub struct GamePreviewPrivate; +pub struct GamePreviewPrivate { + title: gtk::Label, + black_player: gtk::Label, + white_player: gtk::Label, + date: gtk::Label, +} #[glib::object_subclass] impl ObjectSubclass for GamePreviewPrivate { @@ -21,22 +26,26 @@ glib::wrapper! { } impl GamePreview { - pub fn new(element: GamePreviewElement) -> GamePreview { + pub fn new() -> GamePreview { let s: Self = Object::builder().build(); s.set_orientation(gtk::Orientation::Horizontal); + s.set_homogeneous(true); + s.set_hexpand(true); - println!("game_preview: {:?}", element); - let black_player = match element.black_rank { - Some(rank) => format!("{} ({})", element.black_player, rank.to_string()), - None => element.black_player, - }; - let white_player = match element.white_rank { - Some(rank) => format!("{} ({})", element.white_player, rank.to_string()), - None => element.white_player, - }; - s.append(>k::Label::new(Some(&black_player))); - s.append(>k::Label::new(Some(&white_player))); + s.append(&s.imp().date); + s.append(&s.imp().title); + s.append(&s.imp().black_player); + s.append(&s.imp().white_player); s } + + pub fn set_game(&self, element: GamePreviewElement) { + self.imp().black_player.set_text(&element.black_player); + self.imp().white_player.set_text(&element.white_player); + self.imp().title.set_text(&element.name); + if let Some(date) = element.date.first() { + self.imp().date.set_text(&date); + } + } } diff --git a/kifu/gtk/src/ui/home.rs b/kifu/gtk/src/ui/home.rs index 8070a1d..c9a166b 100644 --- a/kifu/gtk/src/ui/home.rs +++ b/kifu/gtk/src/ui/home.rs @@ -1,5 +1,4 @@ -use crate::ui::GamePreview; -use crate::CoreApi; +use crate::{ui::Library, CoreApi}; use glib::Object; use gtk::{glib, prelude::*, subclass::prelude::*}; use kifu_core::{ @@ -101,31 +100,51 @@ impl Default for HomePrivate { impl ObjectSubclass for HomePrivate { const NAME: &'static str = "Home"; type Type = Home; - type ParentType = gtk::Grid; + type ParentType = gtk::Box; } impl ObjectImpl for HomePrivate {} impl WidgetImpl for HomePrivate {} -impl GridImpl for HomePrivate {} +impl BoxImpl for HomePrivate {} glib::wrapper! { - pub struct Home(ObjectSubclass) @extends gtk::Grid, gtk::Widget; + pub struct Home(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; } impl Home { pub fn new(api: CoreApi, view: HomeView) -> Home { 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); - s.attach(&black_player, 1, 1, 1, 1); + players.append(&black_player); *s.imp().black_player.borrow_mut() = Some(black_player.clone()); - let white_player = PlayerDataEntry::new(view.white_player); - s.attach(&white_player, 2, 1, 1, 1); + players.append(&white_player); *s.imp().white_player.borrow_mut() = Some(white_player.clone()); let new_game_button = gtk::Button::builder().label(&view.start_game.label).build(); - s.attach(&new_game_button, 2, 2, 1, 1); + s.append(&new_game_button); + + let library = Library::new(); + 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); + + library.set_games(view.games); new_game_button.connect_clicked({ move |_| { @@ -139,12 +158,6 @@ impl Home { } }); - let game_list = gtk::Box::new(gtk::Orientation::Vertical, 0); - s.attach(&game_list, 1, 3, 2, 1); - view.games - .iter() - .for_each(|game_preview| game_list.append(&GamePreview::new(game_preview.clone()))); - s } } diff --git a/kifu/gtk/src/ui/library.rs b/kifu/gtk/src/ui/library.rs new file mode 100644 index 0000000..9fc24ce --- /dev/null +++ b/kifu/gtk/src/ui/library.rs @@ -0,0 +1,113 @@ +use crate::ui::GamePreview; +use glib::Object; +use gtk::{glib, prelude::*, subclass::prelude::*}; +use kifu_core::ui::GamePreviewElement; +use std::{cell::RefCell, rc::Rc}; + +#[derive(Default)] +pub struct GameObjectPrivate { + game: Rc>>, +} + +#[glib::object_subclass] +impl ObjectSubclass for GameObjectPrivate { + const NAME: &'static str = "GameObject"; + type Type = GameObject; +} + +impl ObjectImpl for GameObjectPrivate {} + +glib::wrapper! { + pub struct GameObject(ObjectSubclass); +} + +impl GameObject { + pub fn new(game: GamePreviewElement) -> Self { + let s: Self = Object::builder().build(); + *s.imp().game.borrow_mut() = Some(game); + s + } + + pub fn game(&self) -> Option { + self.imp().game.borrow().clone() + } +} + +pub struct LibraryPrivate { + model: gio::ListStore, + list_view: gtk::ListView, +} + +impl Default for LibraryPrivate { + fn default() -> Self { + let vector: Vec = vec![]; + let model = gio::ListStore::new(glib::types::Type::OBJECT); + model.extend_from_slice(&vector); + let factory = gtk::SignalListItemFactory::new(); + + factory.connect_setup(move |_, list_item| { + let preview = GamePreview::new(); + list_item + .downcast_ref::() + .expect("Needs to be a ListItem") + .set_child(Some(&preview)); + }); + factory.connect_bind(move |_, list_item| { + let game_element = list_item + .downcast_ref::() + .expect("Needs to be ListItem") + .item() + .and_downcast::() + .expect("The item has to be a GameObject."); + + let preview = list_item + .downcast_ref::() + .expect("Needs to be ListItem") + .child() + .and_downcast::() + .expect("The child has to be a GamePreview object."); + + match game_element.game() { + Some(game) => preview.set_game(game), + None => (), + }; + }); + + let selection_model = gtk::NoSelection::new(Some(model.clone())); + let list_view = gtk::ListView::new(Some(selection_model), Some(factory)); + list_view.set_hexpand(true); + Self { model, list_view } + } +} + +#[glib::object_subclass] +impl ObjectSubclass for LibraryPrivate { + const NAME: &'static str = "Library"; + type Type = Library; + type ParentType = gtk::Box; +} + +impl ObjectImpl for LibraryPrivate {} +impl WidgetImpl for LibraryPrivate {} +impl BoxImpl for LibraryPrivate {} + +glib::wrapper! { + pub struct Library(ObjectSubclass) @extends gtk::Widget, gtk::Box; +} + +impl Library { + pub fn new() -> Self { + let s: Self = Object::builder().build(); + s.set_hexpand(true); + s.append(&s.imp().list_view); + s + } + + pub fn set_games(&self, games: Vec) { + let games = games + .into_iter() + .map(|g| GameObject::new(g)) + .collect::>(); + self.imp().model.extend_from_slice(&games); + } +} diff --git a/kifu/gtk/src/ui/mod.rs b/kifu/gtk/src/ui/mod.rs index d3b9c14..ae70aa6 100644 --- a/kifu/gtk/src/ui/mod.rs +++ b/kifu/gtk/src/ui/mod.rs @@ -4,6 +4,9 @@ pub use chat::Chat; mod game_preview; pub use game_preview::GamePreview; +mod library; +pub use library::Library; + mod player_card; pub use player_card::PlayerCard; diff --git a/sgf/src/date.rs b/sgf/src/date.rs index 4b9ee9d..3058174 100644 --- a/sgf/src/date.rs +++ b/sgf/src/date.rs @@ -24,6 +24,16 @@ pub enum Date { Date(chrono::NaiveDate), } +impl Date { + pub fn to_string(&self) -> String { + match self { + Date::Year(y) => format!("{}", y), + Date::YearMonth(y, m) => format!("{}-{}", y, m), + Date::Date(date) => format!("{}-{}-{}", date.year(), date.month(), date.day()), + } + } +} + /* impl TryFrom<&str> for Date { type Error = String; diff --git a/sgf/src/go.rs b/sgf/src/go.rs index 167b0c2..283ba2d 100644 --- a/sgf/src/go.rs +++ b/sgf/src/go.rs @@ -99,6 +99,11 @@ impl TryFrom for Game { info.app_name = tree.sequence[0] .find_prop("AP") .map(|prop| prop.values[0].clone()); + + info.game_name = tree.sequence[0] + .find_prop("GN") + .map(|prop| prop.values[0].clone()); + info.black_player = tree.sequence[0] .find_prop("PB") .map(|prop| prop.values.join(", "));