diff --git a/Cargo.lock b/Cargo.lock index 7137afc..c57952e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1425,6 +1425,7 @@ dependencies = [ "sgf", "thiserror", "typeshare", + "uuid 1.4.1", ] [[package]] diff --git a/kifu/core/Cargo.toml b/kifu/core/Cargo.toml index 1157a6f..6f8352a 100644 --- a/kifu/core/Cargo.toml +++ b/kifu/core/Cargo.toml @@ -15,6 +15,7 @@ serde_json = { version = "1" } serde = { version = "1", features = [ "derive" ] } thiserror = { version = "1" } typeshare = { version = "1" } +uuid = { version = "1.4", features = [ "v4" ] } [dev-dependencies] cool_asserts = { version = "2" } diff --git a/kifu/core/src/api.rs b/kifu/core/src/api.rs index 0c47e67..1886c8c 100644 --- a/kifu/core/src/api.rs +++ b/kifu/core/src/api.rs @@ -11,6 +11,7 @@ use std::{ sync::{Arc, RwLock}, }; use typeshare::typeshare; +use uuid::Uuid; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[typeshare] @@ -20,7 +21,7 @@ pub enum CoreRequest { CreateGame(CreateGameRequest), Home, OpenConfiguration, - OpenGameReview, + OpenGameReview(GameId), PlayingField, PlayStone(PlayStoneRequest), StartGame, @@ -37,10 +38,16 @@ pub enum CoreResponse { UpdatedConfigurationView(ConfigurationView), } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)] #[typeshare] pub struct GameId(String); +impl GameId { + pub fn new() -> Self { + GameId(Uuid::new_v4().hyphenated().to_string()) + } +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[typeshare] #[serde(tag = "type", content = "content")] @@ -140,7 +147,11 @@ impl CoreApp { CoreRequest::OpenConfiguration => { CoreResponse::ConfigurationView(configuration(&self.config.read().unwrap())) } - CoreRequest::OpenGameReview => CoreResponse::GameReview(review()), + CoreRequest::OpenGameReview(game_id) => { + let state = self.state.read().unwrap(); + let game = state.database.get(&game_id).unwrap(); + CoreResponse::GameReview(review(game)) + } CoreRequest::PlayingField => { let app_state = self.state.read().unwrap(); let game = app_state.game.as_ref().unwrap(); diff --git a/kifu/core/src/database.rs b/kifu/core/src/database.rs index c7e9cc2..ef352b7 100644 --- a/kifu/core/src/database.rs +++ b/kifu/core/src/database.rs @@ -1,8 +1,9 @@ -use std::{ffi::OsStr, io::Read, os::unix::ffi::OsStrExt, path::PathBuf}; - use sgf::{go, parse_sgf, Game}; +use std::{collections::HashMap, ffi::OsStr, io::Read, os::unix::ffi::OsStrExt, path::PathBuf}; use thiserror::Error; +use crate::api::GameId; + #[derive(Error, Debug)] pub enum Error { #[error("Database permission denied")] @@ -20,12 +21,12 @@ impl From for Error { #[derive(Debug)] pub struct Database { path: PathBuf, - games: Vec, + games: HashMap, } impl Database { pub fn open_path(path: PathBuf) -> Result { - let mut games: Vec = Vec::new(); + let mut games: HashMap = HashMap::new(); let extension = PathBuf::from("sgf").into_os_string(); @@ -43,7 +44,9 @@ impl Database { Ok(sgfs) => { for sgf in sgfs { match sgf { - Game::Go(game) => games.push(game), + Game::Go(game) => { + games.insert(GameId::new(), game); + } Game::Unsupported(_) => {} } } @@ -63,9 +66,13 @@ impl Database { self.games.len() } - pub fn all_games(&self) -> impl Iterator { + pub fn all_games(&self) -> impl Iterator { self.games.iter() } + + pub fn get(&self, game_id: &GameId) -> Option<&go::Game> { + self.games.get(game_id) + } } #[cfg(test)] diff --git a/kifu/core/src/ui/elements/game_preview.rs b/kifu/core/src/ui/elements/game_preview.rs index fbd00ea..67ba865 100644 --- a/kifu/core/src/ui/elements/game_preview.rs +++ b/kifu/core/src/ui/elements/game_preview.rs @@ -2,9 +2,12 @@ use serde::{Deserialize, Serialize}; use sgf::go::{Game, GameResult, Win}; use typeshare::typeshare; +use crate::api::GameId; + #[derive(Clone, Debug, Deserialize, Serialize)] #[typeshare] pub struct GamePreviewElement { + pub id: GameId, pub date: String, pub name: String, pub black_player: String, @@ -13,7 +16,7 @@ pub struct GamePreviewElement { } impl GamePreviewElement { - pub fn new(game: &Game) -> GamePreviewElement { + pub fn new(id: &GameId, game: &Game) -> GamePreviewElement { let black_player = match game.info.black_player { Some(ref black_player) => black_player.clone(), None => "unknown".to_owned(), @@ -55,6 +58,7 @@ impl GamePreviewElement { }; GamePreviewElement { + id: id.clone(), date: game .info .date diff --git a/kifu/core/src/ui/game_review.rs b/kifu/core/src/ui/game_review.rs index e695227..ccc2019 100644 --- a/kifu/core/src/ui/game_review.rs +++ b/kifu/core/src/ui/game_review.rs @@ -26,7 +26,7 @@ pub struct Node { pub children: Vec, } -pub fn review() -> GameReviewView { +pub fn review(game: &Game) -> GameReviewView { GameReviewView { black_player: "savanni".to_owned(), white_player: "kat".to_owned(), diff --git a/kifu/core/src/ui/home.rs b/kifu/core/src/ui/home.rs index 59a064b..7e4c37e 100644 --- a/kifu/core/src/ui/home.rs +++ b/kifu/core/src/ui/home.rs @@ -1,4 +1,7 @@ -use crate::ui::{Action, GamePreviewElement}; +use crate::{ + api::GameId, + ui::{Action, GamePreviewElement}, +}; use serde::{Deserialize, Serialize}; use sgf::go::Game; use typeshare::typeshare; @@ -56,7 +59,7 @@ pub struct HomeView { pub start_game: Action<()>, } -pub fn home<'a>(games: impl Iterator) -> HomeView { +pub fn home<'a>(games: impl Iterator) -> HomeView { let black_player = PlayerElement::Hotseat(HotseatPlayerElement { placeholder: Some("black player".to_owned()), default_rank: None, @@ -70,7 +73,9 @@ pub fn home<'a>(games: impl Iterator) -> HomeView { HomeView { black_player, white_player, - games: games.map(GamePreviewElement::new).collect(), + games: games + .map(|(id, game)| GamePreviewElement::new(id, game)) + .collect(), start_game: Action { id: "start-game-action".to_owned(), label: "New Game".to_owned(), diff --git a/kifu/core/src/ui/mod.rs b/kifu/core/src/ui/mod.rs index a662877..d91907f 100644 --- a/kifu/core/src/ui/mod.rs +++ b/kifu/core/src/ui/mod.rs @@ -5,7 +5,7 @@ mod elements; pub use elements::{game_preview::GamePreviewElement, menu::Menu, Action, Field, Toggle}; mod game_review; -pub use game_review::{review, GameReviewView}; +pub use game_review::{review, GameReviewView, Node}; mod playing_field; pub use playing_field::{playing_field, PlayingFieldView}; diff --git a/kifu/gtk/src/ui/game_review.rs b/kifu/gtk/src/ui/game_review.rs index a9d22ba..b8c0d00 100644 --- a/kifu/gtk/src/ui/game_review.rs +++ b/kifu/gtk/src/ui/game_review.rs @@ -1,4 +1,7 @@ -use crate::{ui::Board, CoreApi}; +use crate::{ + ui::{Board, ReviewTree}, + CoreApi, +}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use kifu_core::ui::GameReviewView; @@ -7,6 +10,7 @@ use std::{cell::RefCell, rc::Rc}; #[derive(Default)] pub struct GameReviewPrivate { board: Rc>>, + tree: ReviewTree, } #[glib::object_subclass] @@ -29,6 +33,18 @@ glib::wrapper! { impl GameReview { pub fn new(api: CoreApi, view: GameReviewView) -> Self { let s: Self = Object::builder().build(); + s.set_orientation(gtk::Orientation::Horizontal); + + let review_area = gtk::ScrolledWindow::builder() + .hscrollbar_policy(gtk::PolicyType::Automatic) + .vscrollbar_policy(gtk::PolicyType::Automatic) + .hexpand(true) + .vexpand(true) + .build(); + review_area.set_child(Some(&s.imp().tree)); + s.append(&review_area); + + s.imp().tree.set_tree(view.tree); s } diff --git a/kifu/gtk/src/ui/library.rs b/kifu/gtk/src/ui/library.rs index 1f32ee8..076507c 100644 --- a/kifu/gtk/src/ui/library.rs +++ b/kifu/gtk/src/ui/library.rs @@ -1,5 +1,6 @@ use crate::{ui::GamePreview, CoreApi}; use adw::{prelude::*, subclass::prelude::*}; +use gio::ListModel; use glib::Object; use gtk::{glib, prelude::*, subclass::prelude::*}; use kifu_core::ui::GamePreviewElement; @@ -121,8 +122,18 @@ impl Library { let s: Self = Object::builder().build(); s.set_child(Some(&s.imp().list_view)); - s.imp().list_view.connect_activate(move |list, row_id| { - api.dispatch(kifu_core::CoreRequest::OpenGameReview); + s.imp().list_view.connect_activate({ + let s = s.clone(); + move |_, row_id| { + let object = s.imp().model.item(row_id).unwrap(); + // let list_item = object.downcast_ref::().unwrap(); + // let game = list_item.item().and_downcast::().unwrap(); + // let game_id = game.game().unwrap().id; + // let game = object.downcast_ref::() + let game = object.downcast::().unwrap(); + let game_id = game.game().unwrap().id; + api.dispatch(kifu_core::CoreRequest::OpenGameReview(game_id)); + } }); s } diff --git a/kifu/gtk/src/ui/review_tree.rs b/kifu/gtk/src/ui/review_tree.rs index 21fc55b..b215ebb 100644 --- a/kifu/gtk/src/ui/review_tree.rs +++ b/kifu/gtk/src/ui/review_tree.rs @@ -1,30 +1,139 @@ use adw::{prelude::*, subclass::prelude::*}; +use cairo::Context; use glib::Object; -use std::{cell::Cell, rc::Rc}; +use kifu_core::ui::Node; +use std::{cell::RefCell, f64::consts::PI, rc::Rc}; + +const NODE_SIZE: f64 = 10.; +const NODE_ROW_HEIGHT: f64 = 30.; +const NODE_COLUMN_WIDTH: f64 = 30.; #[derive(Default)] -pub struct ReviewTreePrivate {} +pub struct ReviewTreePrivate { + tree: Rc>>, +} #[glib::object_subclass] impl ObjectSubclass for ReviewTreePrivate { const NAME: &'static str = "ReviewTree"; type Type = ReviewTree; - type ParentType = adw::Bin; + type ParentType = gtk::DrawingArea; } impl ObjectImpl for ReviewTreePrivate {} impl WidgetImpl for ReviewTreePrivate {} -impl BinImpl for ReviewTreePrivate {} +impl DrawingAreaImpl for ReviewTreePrivate {} glib::wrapper! { pub struct ReviewTree(ObjectSubclass) - @extends adw::Bin, gtk::Widget; + @extends gtk::DrawingArea, gtk::Widget; } impl ReviewTree { pub fn new() -> Self { let s: Self = Object::builder().build(); + s.set_draw_func({ + let s = s.clone(); + move |_, context, width, height| { + println!("drawing area: {} {}", width, height); + let style_context = WidgetExt::style_context(&s); + let bg = style_context.lookup_color("view_bg_color").unwrap(); + let fg = style_context.lookup_color("view_fg_color").unwrap(); + + context.set_source_rgb(bg.red() as f64, bg.green() as f64, bg.blue() as f64); + let _ = context.paint(); + + if let Some(tree) = &*s.imp().tree.borrow() { + let (width, height) = max_tree_dimensions(tree); + println!("tree dimensions: {} {}", width, height); + s.set_width_request( + width as i32 * (NODE_SIZE as i32 + NODE_COLUMN_WIDTH as i32) + 20, + ); + s.set_height_request( + height as i32 * (NODE_SIZE as i32 + NODE_ROW_HEIGHT as i32) + 20, + ); + + context.set_source_rgb(fg.red() as f64, fg.green() as f64, fg.blue() as f64); + draw_tree(&context, &tree); + } + } + }); + s } + + pub fn set_tree(&self, tree: Node) { + *self.imp().tree.borrow_mut() = Some(tree); + self.queue_draw(); + } +} + +impl Default for ReviewTree { + fn default() -> Self { + Self::new() + } +} + +fn draw_node(context: &Context, x: f64, y: f64) { + context.arc(x, y, NODE_SIZE, 0., 2. * PI); + let _ = context.fill(); +} + +fn draw_tree(context: &Context, tree: &Node) { + let mut row: Vec<&Node> = vec![]; + let mut next_row: Vec<&Node> = vec![]; + let mut width: Vec = vec![]; + + let mut x = 0; + let mut y = 0; + + row.push(tree); + width.push(1); + + while row.len() != 0 { + for node in row.into_iter() { + draw_node( + context, + 10. + (x as f64) * (NODE_SIZE + NODE_COLUMN_WIDTH), + 10. + (y as f64) * (NODE_SIZE + NODE_ROW_HEIGHT), + ); + next_row.append(&mut node.children.iter().map(|n| n).collect::>()); + x = x + 1; + } + x = 0; + y = y + 1; + row = next_row; + next_row = vec![]; + } +} + +fn max_tree_dimensions(tree: &Node) -> (usize, usize) { + let mut row: Vec<&Node> = vec![]; + let mut next_row: Vec<&Node> = vec![]; + let mut width: Vec = vec![]; + + row.push(tree); + width.push(1); + + while row.len() != 0 { + println!("new row"); + for node in row.into_iter() { + println!( + "{:?} {:?} {}", + node.color, + node.position, + node.children.len() + ); + next_row.append(&mut node.children.iter().map(|n| n).collect::>()); + } + width.push(next_row.len()); + row = next_row; + next_row = vec![]; + } + + ( + width.iter().fold(0, |a, b| if a > *b { a } else { *b }), + width.len(), + ) }