Start trying to draw the game tree
This commit is contained in:
parent
d85d9c4593
commit
6bf40684b9
|
@ -1425,6 +1425,7 @@ dependencies = [
|
||||||
"sgf",
|
"sgf",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"typeshare",
|
"typeshare",
|
||||||
|
"uuid 1.4.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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};
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue