Start trying to draw the game tree

This commit is contained in:
Savanni D'Gerinel 2023-08-29 22:44:11 -04:00
parent d85d9c4593
commit 6bf40684b9
11 changed files with 188 additions and 23 deletions

1
Cargo.lock generated
View File

@ -1425,6 +1425,7 @@ dependencies = [
"sgf", "sgf",
"thiserror", "thiserror",
"typeshare", "typeshare",
"uuid 1.4.1",
] ]
[[package]] [[package]]

View File

@ -15,6 +15,7 @@ serde_json = { version = "1" }
serde = { version = "1", features = [ "derive" ] } serde = { version = "1", features = [ "derive" ] }
thiserror = { version = "1" } thiserror = { version = "1" }
typeshare = { version = "1" } typeshare = { version = "1" }
uuid = { version = "1.4", features = [ "v4" ] }
[dev-dependencies] [dev-dependencies]
cool_asserts = { version = "2" } cool_asserts = { version = "2" }

View File

@ -11,6 +11,7 @@ use std::{
sync::{Arc, RwLock}, sync::{Arc, RwLock},
}; };
use typeshare::typeshare; use typeshare::typeshare;
use uuid::Uuid;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[typeshare] #[typeshare]
@ -20,7 +21,7 @@ pub enum CoreRequest {
CreateGame(CreateGameRequest), CreateGame(CreateGameRequest),
Home, Home,
OpenConfiguration, OpenConfiguration,
OpenGameReview, OpenGameReview(GameId),
PlayingField, PlayingField,
PlayStone(PlayStoneRequest), PlayStone(PlayStoneRequest),
StartGame, StartGame,
@ -37,10 +38,16 @@ pub enum CoreResponse {
UpdatedConfigurationView(ConfigurationView), UpdatedConfigurationView(ConfigurationView),
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
#[typeshare] #[typeshare]
pub struct GameId(String); pub struct GameId(String);
impl GameId {
pub fn new() -> Self {
GameId(Uuid::new_v4().hyphenated().to_string())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[typeshare] #[typeshare]
#[serde(tag = "type", content = "content")] #[serde(tag = "type", content = "content")]
@ -140,7 +147,11 @@ impl CoreApp {
CoreRequest::OpenConfiguration => { CoreRequest::OpenConfiguration => {
CoreResponse::ConfigurationView(configuration(&self.config.read().unwrap())) 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 => { CoreRequest::PlayingField => {
let app_state = self.state.read().unwrap(); let app_state = self.state.read().unwrap();
let game = app_state.game.as_ref().unwrap(); let game = app_state.game.as_ref().unwrap();

View File

@ -1,8 +1,9 @@
use std::{ffi::OsStr, io::Read, os::unix::ffi::OsStrExt, path::PathBuf};
use sgf::{go, parse_sgf, Game}; use sgf::{go, parse_sgf, Game};
use std::{collections::HashMap, ffi::OsStr, io::Read, os::unix::ffi::OsStrExt, path::PathBuf};
use thiserror::Error; use thiserror::Error;
use crate::api::GameId;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
#[error("Database permission denied")] #[error("Database permission denied")]
@ -20,12 +21,12 @@ impl From<std::io::Error> for Error {
#[derive(Debug)] #[derive(Debug)]
pub struct Database { pub struct Database {
path: PathBuf, path: PathBuf,
games: Vec<go::Game>, games: HashMap<GameId, go::Game>,
} }
impl Database { impl Database {
pub fn open_path(path: PathBuf) -> Result<Database, Error> { pub fn open_path(path: PathBuf) -> Result<Database, Error> {
let mut games: Vec<go::Game> = Vec::new(); let mut games: HashMap<GameId, go::Game> = HashMap::new();
let extension = PathBuf::from("sgf").into_os_string(); let extension = PathBuf::from("sgf").into_os_string();
@ -43,7 +44,9 @@ impl Database {
Ok(sgfs) => { Ok(sgfs) => {
for sgf in sgfs { for sgf in sgfs {
match sgf { match sgf {
Game::Go(game) => games.push(game), Game::Go(game) => {
games.insert(GameId::new(), game);
}
Game::Unsupported(_) => {} Game::Unsupported(_) => {}
} }
} }
@ -63,9 +66,13 @@ impl Database {
self.games.len() self.games.len()
} }
pub fn all_games(&self) -> impl Iterator<Item = &go::Game> { pub fn all_games(&self) -> impl Iterator<Item = (&GameId, &go::Game)> {
self.games.iter() self.games.iter()
} }
pub fn get(&self, game_id: &GameId) -> Option<&go::Game> {
self.games.get(game_id)
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -2,9 +2,12 @@ use serde::{Deserialize, Serialize};
use sgf::go::{Game, GameResult, Win}; use sgf::go::{Game, GameResult, Win};
use typeshare::typeshare; use typeshare::typeshare;
use crate::api::GameId;
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
#[typeshare] #[typeshare]
pub struct GamePreviewElement { pub struct GamePreviewElement {
pub id: GameId,
pub date: String, pub date: String,
pub name: String, pub name: String,
pub black_player: String, pub black_player: String,
@ -13,7 +16,7 @@ pub struct GamePreviewElement {
} }
impl 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 { let black_player = match game.info.black_player {
Some(ref black_player) => black_player.clone(), Some(ref black_player) => black_player.clone(),
None => "unknown".to_owned(), None => "unknown".to_owned(),
@ -55,6 +58,7 @@ impl GamePreviewElement {
}; };
GamePreviewElement { GamePreviewElement {
id: id.clone(),
date: game date: game
.info .info
.date .date

View File

@ -26,7 +26,7 @@ pub struct Node {
pub children: Vec<Node>, pub children: Vec<Node>,
} }
pub fn review() -> GameReviewView { pub fn review(game: &Game) -> GameReviewView {
GameReviewView { GameReviewView {
black_player: "savanni".to_owned(), black_player: "savanni".to_owned(),
white_player: "kat".to_owned(), white_player: "kat".to_owned(),

View File

@ -1,4 +1,7 @@
use crate::ui::{Action, GamePreviewElement}; use crate::{
api::GameId,
ui::{Action, GamePreviewElement},
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sgf::go::Game; use sgf::go::Game;
use typeshare::typeshare; use typeshare::typeshare;
@ -56,7 +59,7 @@ pub struct HomeView {
pub start_game: Action<()>, pub start_game: Action<()>,
} }
pub fn home<'a>(games: impl Iterator<Item = &'a Game>) -> HomeView { pub fn home<'a>(games: impl Iterator<Item = (&'a GameId, &'a Game)>) -> HomeView {
let black_player = PlayerElement::Hotseat(HotseatPlayerElement { let black_player = PlayerElement::Hotseat(HotseatPlayerElement {
placeholder: Some("black player".to_owned()), placeholder: Some("black player".to_owned()),
default_rank: None, default_rank: None,
@ -70,7 +73,9 @@ pub fn home<'a>(games: impl Iterator<Item = &'a Game>) -> HomeView {
HomeView { HomeView {
black_player, black_player,
white_player, white_player,
games: games.map(GamePreviewElement::new).collect(), games: games
.map(|(id, game)| GamePreviewElement::new(id, game))
.collect(),
start_game: Action { start_game: Action {
id: "start-game-action".to_owned(), id: "start-game-action".to_owned(),
label: "New Game".to_owned(), label: "New Game".to_owned(),

View File

@ -5,7 +5,7 @@ mod elements;
pub use elements::{game_preview::GamePreviewElement, menu::Menu, Action, Field, Toggle}; pub use elements::{game_preview::GamePreviewElement, menu::Menu, Action, Field, Toggle};
mod game_review; mod game_review;
pub use game_review::{review, GameReviewView}; pub use game_review::{review, GameReviewView, Node};
mod playing_field; mod playing_field;
pub use playing_field::{playing_field, PlayingFieldView}; pub use playing_field::{playing_field, PlayingFieldView};

View File

@ -1,4 +1,7 @@
use crate::{ui::Board, CoreApi}; use crate::{
ui::{Board, ReviewTree},
CoreApi,
};
use glib::Object; use glib::Object;
use gtk::{prelude::*, subclass::prelude::*}; use gtk::{prelude::*, subclass::prelude::*};
use kifu_core::ui::GameReviewView; use kifu_core::ui::GameReviewView;
@ -7,6 +10,7 @@ use std::{cell::RefCell, rc::Rc};
#[derive(Default)] #[derive(Default)]
pub struct GameReviewPrivate { pub struct GameReviewPrivate {
board: Rc<RefCell<Option<Board>>>, board: Rc<RefCell<Option<Board>>>,
tree: ReviewTree,
} }
#[glib::object_subclass] #[glib::object_subclass]
@ -29,6 +33,18 @@ glib::wrapper! {
impl GameReview { impl GameReview {
pub fn new(api: CoreApi, view: GameReviewView) -> Self { pub fn new(api: CoreApi, view: GameReviewView) -> Self {
let s: Self = Object::builder().build(); 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 s
} }

View File

@ -1,5 +1,6 @@
use crate::{ui::GamePreview, CoreApi}; use crate::{ui::GamePreview, CoreApi};
use adw::{prelude::*, subclass::prelude::*}; use adw::{prelude::*, subclass::prelude::*};
use gio::ListModel;
use glib::Object; use glib::Object;
use gtk::{glib, prelude::*, subclass::prelude::*}; use gtk::{glib, prelude::*, subclass::prelude::*};
use kifu_core::ui::GamePreviewElement; use kifu_core::ui::GamePreviewElement;
@ -121,8 +122,18 @@ impl Library {
let s: Self = Object::builder().build(); let s: Self = Object::builder().build();
s.set_child(Some(&s.imp().list_view)); s.set_child(Some(&s.imp().list_view));
s.imp().list_view.connect_activate(move |list, row_id| { s.imp().list_view.connect_activate({
api.dispatch(kifu_core::CoreRequest::OpenGameReview); let s = s.clone();
move |_, row_id| {
let object = s.imp().model.item(row_id).unwrap();
// let list_item = object.downcast_ref::<gtk::ListItem>().unwrap();
// let game = list_item.item().and_downcast::<GameObject>().unwrap();
// let game_id = game.game().unwrap().id;
// let game = object.downcast_ref::<gtk::ListItem>()
let game = object.downcast::<GameObject>().unwrap();
let game_id = game.game().unwrap().id;
api.dispatch(kifu_core::CoreRequest::OpenGameReview(game_id));
}
}); });
s s
} }

View File

@ -1,30 +1,139 @@
use adw::{prelude::*, subclass::prelude::*}; use adw::{prelude::*, subclass::prelude::*};
use cairo::Context;
use glib::Object; 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)] #[derive(Default)]
pub struct ReviewTreePrivate {} pub struct ReviewTreePrivate {
tree: Rc<RefCell<Option<Node>>>,
}
#[glib::object_subclass] #[glib::object_subclass]
impl ObjectSubclass for ReviewTreePrivate { impl ObjectSubclass for ReviewTreePrivate {
const NAME: &'static str = "ReviewTree"; const NAME: &'static str = "ReviewTree";
type Type = ReviewTree; type Type = ReviewTree;
type ParentType = adw::Bin; type ParentType = gtk::DrawingArea;
} }
impl ObjectImpl for ReviewTreePrivate {} impl ObjectImpl for ReviewTreePrivate {}
impl WidgetImpl for ReviewTreePrivate {} impl WidgetImpl for ReviewTreePrivate {}
impl BinImpl for ReviewTreePrivate {} impl DrawingAreaImpl for ReviewTreePrivate {}
glib::wrapper! { glib::wrapper! {
pub struct ReviewTree(ObjectSubclass<ReviewTreePrivate>) pub struct ReviewTree(ObjectSubclass<ReviewTreePrivate>)
@extends adw::Bin, gtk::Widget; @extends gtk::DrawingArea, gtk::Widget;
} }
impl ReviewTree { impl ReviewTree {
pub fn new() -> Self { pub fn new() -> Self {
let s: Self = Object::builder().build(); 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 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<usize> = 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::<Vec<&Node>>());
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<usize> = 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::<Vec<&Node>>());
}
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(),
)
} }