Compare commits

...

2 Commits

Author SHA1 Message Date
Savanni D'Gerinel 6bf40684b9 Start trying to draw the game tree 2023-08-29 23:22:04 -04:00
Savanni D'Gerinel d85d9c4593 Start on the game review window 2023-08-29 23:22:04 -04:00
15 changed files with 349 additions and 76 deletions

1
Cargo.lock generated
View File

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

View File

@ -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" }

View File

@ -1,6 +1,9 @@
use crate::{
types::{AppState, Config, ConfigOption, DatabasePath, GameState, Player, Rank},
ui::{configuration, home, playing_field, ConfigurationView, HomeView, PlayingFieldView},
ui::{
configuration, home, playing_field, review, ConfigurationView, GameReviewView, HomeView,
PlayingFieldView,
},
};
use serde::{Deserialize, Serialize};
use std::{
@ -8,6 +11,7 @@ use std::{
sync::{Arc, RwLock},
};
use typeshare::typeshare;
use uuid::Uuid;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[typeshare]
@ -17,11 +21,33 @@ pub enum CoreRequest {
CreateGame(CreateGameRequest),
Home,
OpenConfiguration,
OpenGameReview(GameId),
PlayingField,
PlayStone(PlayStoneRequest),
StartGame,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[typeshare]
#[serde(tag = "type", content = "content")]
pub enum CoreResponse {
ConfigurationView(ConfigurationView),
HomeView(HomeView),
GameReview(GameReviewView),
PlayingFieldView(PlayingFieldView),
UpdatedConfigurationView(ConfigurationView),
}
#[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")]
@ -65,16 +91,6 @@ impl From<HotseatPlayerRequest> for Player {
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[typeshare]
#[serde(tag = "type", content = "content")]
pub enum CoreResponse {
ConfigurationView(ConfigurationView),
HomeView(HomeView),
PlayingFieldView(PlayingFieldView),
UpdatedConfigurationView(ConfigurationView),
}
#[derive(Clone, Debug)]
pub struct CoreApp {
config: Arc<RwLock<Config>>,
@ -131,6 +147,11 @@ impl CoreApp {
CoreRequest::OpenConfiguration => {
CoreResponse::ConfigurationView(configuration(&self.config.read().unwrap()))
}
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();

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 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<std::io::Error> for Error {
#[derive(Debug)]
pub struct Database {
path: PathBuf,
games: Vec<go::Game>,
games: HashMap<GameId, go::Game>,
}
impl Database {
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();
@ -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<Item = &go::Game> {
pub fn all_games(&self) -> impl Iterator<Item = (&GameId, &go::Game)> {
self.games.iter()
}
pub fn get(&self, game_id: &GameId) -> Option<&go::Game> {
self.games.get(game_id)
}
}
#[cfg(test)]

View File

@ -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

View File

@ -0,0 +1,50 @@
use crate::Color;
use serde::{Deserialize, Serialize};
use sgf::go::Game;
use typeshare::typeshare;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[typeshare]
pub struct GameReviewView {
pub black_player: String,
pub white_player: String,
pub tree: Node,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[typeshare]
pub struct Position {
pub column: u8,
pub row: u8,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[typeshare]
pub struct Node {
pub color: Color,
pub position: Position,
pub children: Vec<Node>,
}
pub fn review(game: &Game) -> GameReviewView {
GameReviewView {
black_player: "savanni".to_owned(),
white_player: "kat".to_owned(),
tree: Node {
color: Color::Black,
position: Position { column: 3, row: 3 },
children: vec![Node {
color: Color::White,
position: Position {
column: 15,
row: 15,
},
children: vec![Node {
color: Color::Black,
position: Position { column: 15, row: 3 },
children: vec![],
}],
}],
},
}
}

View File

@ -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<Item = &'a Game>) -> HomeView {
pub fn home<'a>(games: impl Iterator<Item = (&'a GameId, &'a Game)>) -> 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<Item = &'a Game>) -> 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(),

View File

@ -4,6 +4,9 @@ pub use configuration::{configuration, ConfigurationView};
mod elements;
pub use elements::{game_preview::GamePreviewElement, menu::Menu, Action, Field, Toggle};
mod game_review;
pub use game_review::{review, GameReviewView, Node};
mod playing_field;
pub use playing_field::{playing_field, PlayingFieldView};

View File

@ -2,7 +2,7 @@ use adw::prelude::*;
use kifu_core::{CoreApp, CoreRequest, CoreResponse};
use kifu_gtk::{
perftrace,
ui::{AppWindow, ConfigurationPage, Home, PlayingField},
ui::{AppWindow, ConfigurationPage, GameReview, Home, PlayingField},
CoreApi,
};
use std::sync::{Arc, RwLock};
@ -18,6 +18,9 @@ fn handle_response(api: CoreApi, app_window: &AppWindow, message: CoreResponse)
window.set_visible_page(&config_page);
window.present();
}),
CoreResponse::GameReview(view) => perftrace("GameReview", || {
app_window.set_content(&GameReview::new(api, view));
}),
CoreResponse::HomeView(view) => perftrace("HomeView", || {
let api = api.clone();

View File

@ -0,0 +1,51 @@
use crate::{
ui::{Board, ReviewTree},
CoreApi,
};
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*};
use kifu_core::ui::GameReviewView;
use std::{cell::RefCell, rc::Rc};
#[derive(Default)]
pub struct GameReviewPrivate {
board: Rc<RefCell<Option<Board>>>,
tree: ReviewTree,
}
#[glib::object_subclass]
impl ObjectSubclass for GameReviewPrivate {
const NAME: &'static str = "GameReview";
type Type = GameReview;
type ParentType = gtk::Box;
}
impl ObjectImpl for GameReviewPrivate {}
impl WidgetImpl for GameReviewPrivate {}
impl BoxImpl for GameReviewPrivate {}
glib::wrapper! {
pub struct GameReview(ObjectSubclass<GameReviewPrivate>)
@extends gtk::Box, gtk::Widget,
@implements gtk::Orientable;
}
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
}
}

View File

@ -137,7 +137,7 @@ impl Home {
.build();
s.append(&new_game_button);
let library = Library::new();
let library = Library::new(api.clone());
let library_view = gtk::ScrolledWindow::builder()
.hscrollbar_policy(gtk::PolicyType::Never)
.min_content_width(360)

View File

@ -1,5 +1,6 @@
use crate::ui::GamePreview;
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;
@ -45,38 +46,6 @@ impl Default for LibraryPrivate {
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::<gtk::ListItem>()
.expect("Needs to be a ListItem")
.set_child(Some(&preview));
});
factory.connect_bind(move |_, list_item| {
let game_element = list_item
.downcast_ref::<gtk::ListItem>()
.expect("Needs to be ListItem")
.item()
.and_downcast::<GameObject>()
.expect("The item has to be a GameObject.");
let preview = list_item
.downcast_ref::<gtk::ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<GamePreview>()
.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::ColumnView::builder().model(&selection_model).build();
@ -86,15 +55,15 @@ impl Default for LibraryPrivate {
{
let factory = gtk::SignalListItemFactory::new();
factory.connect_setup(|_, list_item| {
list_item
.downcast_ref::<gtk::ListItem>()
.unwrap()
.set_child(Some(
&gtk::Label::builder()
.halign(gtk::Align::Start)
.ellipsize(pango::EllipsizeMode::End)
.build(),
))
let item = list_item.downcast_ref::<gtk::ListItem>().unwrap();
item.set_activatable(true);
item.set_child(Some(
&gtk::Label::builder()
.halign(gtk::Align::Start)
.ellipsize(pango::EllipsizeMode::End)
.build(),
))
});
factory.connect_bind(move |_, list_item| {
let list_item = list_item.downcast_ref::<gtk::ListItem>().unwrap();
@ -149,10 +118,23 @@ glib::wrapper! {
}
impl Library {
pub fn new() -> Self {
pub fn new(api: CoreApi) -> Self {
let s: Self = Object::builder().build();
s.set_child(Some(&s.imp().list_view));
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::<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
}

View File

@ -3,6 +3,9 @@ use gio::resources_lookup_data;
use glib::IsA;
use gtk::{prelude::*, STYLE_PROVIDER_PRIORITY_USER};
mod board;
pub use board::Board;
mod chat;
pub use chat::Chat;
@ -12,6 +15,12 @@ pub use config::ConfigurationPage;
mod game_preview;
pub use game_preview::GamePreview;
mod game_review;
pub use game_review::GameReview;
mod home;
pub use home::Home;
mod library;
pub use library::Library;
@ -21,11 +30,8 @@ pub use player_card::PlayerCard;
mod playing_field;
pub use playing_field::PlayingField;
mod home;
pub use home::Home;
mod board;
pub use board::Board;
mod review_tree;
pub use review_tree::ReviewTree;
#[cfg(feature = "screenplay")]
pub use playing_field::playing_field_view;

View File

@ -12,7 +12,7 @@ pub struct PlayingFieldPrivate {
board: Rc<RefCell<Option<Board>>>,
player_card_white: Rc<RefCell<Option<PlayerCard>>>,
player_card_black: Rc<RefCell<Option<PlayerCard>>>,
chat: Rc<RefCell<Option<Chat>>>,
// chat: Rc<RefCell<Option<Chat>>>,
}
impl Default for PlayingFieldPrivate {
@ -21,7 +21,7 @@ impl Default for PlayingFieldPrivate {
board: Default::default(),
player_card_white: Rc::new(RefCell::new(None)),
player_card_black: Rc::new(RefCell::new(None)),
chat: Rc::new(RefCell::new(None)),
// chat: Rc::new(RefCell::new(None)),
}
}
}
@ -47,7 +47,7 @@ impl PlayingField {
let player_card_white = PlayerCard::new(view.player_card_white);
let player_card_black = PlayerCard::new(view.player_card_black);
let chat = Chat::new(view.chat);
// let chat = Chat::new(view.chat);
*s.imp().board.borrow_mut() = Some(Board::new(api));
s.imp()
@ -57,11 +57,11 @@ impl PlayingField {
.map(|board| s.attach(board, 1, 1, 1, 2));
s.attach(&player_card_black, 2, 1, 1, 1);
s.attach(&player_card_white, 3, 1, 1, 1);
s.attach(&chat, 2, 2, 2, 1);
// s.attach(&chat, 2, 2, 2, 1);
*s.imp().player_card_white.borrow_mut() = Some(player_card_white);
*s.imp().player_card_black.borrow_mut() = Some(player_card_black);
*s.imp().chat.borrow_mut() = Some(chat);
// *s.imp().chat.borrow_mut() = Some(chat);
s.imp().board.borrow().as_ref().map(|board| {
board.set_board(view.board);

View File

@ -0,0 +1,139 @@
use adw::{prelude::*, subclass::prelude::*};
use cairo::Context;
use glib::Object;
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 {
tree: Rc<RefCell<Option<Node>>>,
}
#[glib::object_subclass]
impl ObjectSubclass for ReviewTreePrivate {
const NAME: &'static str = "ReviewTree";
type Type = ReviewTree;
type ParentType = gtk::DrawingArea;
}
impl ObjectImpl for ReviewTreePrivate {}
impl WidgetImpl for ReviewTreePrivate {}
impl DrawingAreaImpl for ReviewTreePrivate {}
glib::wrapper! {
pub struct ReviewTree(ObjectSubclass<ReviewTreePrivate>)
@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<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(),
)
}