diff --git a/otg/core/src/api.rs b/otg/core/src/api.rs index 153984b..9fe080c 100644 --- a/otg/core/src/api.rs +++ b/otg/core/src/api.rs @@ -115,8 +115,6 @@ pub struct Core { impl Core { pub fn new(config: Config) -> Self { - println!("config: {:?}", config); - let library = match config.get::() { Some(ref path) if path.to_path_buf().exists() => { Some(Database::open_path(path.to_path_buf()).unwrap()) diff --git a/otg/core/src/goban.rs b/otg/core/src/goban.rs index edff8b1..ab0d0d8 100644 --- a/otg/core/src/goban.rs +++ b/otg/core/src/goban.rs @@ -213,7 +213,7 @@ impl Goban { /// /// assert_eq!(goban.stone(&Coordinate{ row: 3, column: 3 }), Some(Color::Black)); /// assert_eq!(goban.stone(&Coordinate{ row: 15, column: 15 }), Some(Color::White)); - /// assert_eq!(goban.stone(&Coordinate{ row: 3, column: 15 }), Some(Color::Black)); + /// assert_eq!(goban.stone(&Coordinate{ row: 15, column: 3 }), Some(Color::Black)); /// ``` pub fn apply_moves<'a>( self, @@ -611,7 +611,6 @@ mod test { ), ]; - println!("{}", board); for (board, coordinate, group, liberties) in test_cases { assert_eq!(board.group(&coordinate), group.as_ref()); assert_eq!( diff --git a/otg/core/src/lib.rs b/otg/core/src/lib.rs index 5964ce9..996e642 100644 --- a/otg/core/src/lib.rs +++ b/otg/core/src/lib.rs @@ -32,3 +32,6 @@ mod types; pub use types::{ BoardError, Color, Config, ConfigOption, DepthTree, LibraryPath, Player, Rank, Size, }; + +mod view_models; +pub use view_models::GameReviewViewModel; diff --git a/otg/core/src/types.rs b/otg/core/src/types.rs index c89b55c..b2dc076 100644 --- a/otg/core/src/types.rs +++ b/otg/core/src/types.rs @@ -1,6 +1,7 @@ use crate::goban::{Coordinate, Goban}; use config::define_config; use config_derive::ConfigOption; +use nary_tree::NodeRef; use serde::{Deserialize, Serialize}; use sgf::GameTree; use std::{ @@ -242,6 +243,39 @@ pub struct Tree { } */ +// https://llimllib.github.io/pymag-trees/ +// I want to take advantage of the Wetherell Shannon algorithm, but I want some variations. In +// their diagram, they got a tree that looks like this. +// +// O +// |\ +// O O +// |\ \ \ +// O O O O +// |\ |\ +// O O O O +// +// In the same circumstance, what I want is this: +// +// O-- +// | \ +// O O +// |\ |\ +// O O O O +// |\ +// O O +// +// In order to keep things from being overly smooshed, I want to ensure that if a branch overlaps +// with another branch, there is some extra drawing space. This might actually be similar to adding +// the principal that "A parent should be centered over its children". +// +// So, given a tree, I need to know how many children exist at each level. Then I build parents +// atop the children. At level 3, I have four children, and that happens to be the maximum width of +// the graph. +// +// A bottom-up traversal: +// - Figure out the number of nodes at each depth + pub struct DepthTree(nary_tree::Tree); impl Deref for DepthTree { @@ -252,12 +286,17 @@ impl Deref for DepthTree { } } +impl Default for DepthTree { + fn default() -> Self { + Self(nary_tree::Tree::new()) + } +} + #[derive(Debug)] pub struct SizeNode { /// Use this to map back to the node in the original game tree. This way we know how to /// correspond from a node in the review tree back to there. - #[allow(dead_code)] - game_node_id: nary_tree::NodeId, + pub game_node_id: nary_tree::NodeId, /// How deep into the tree is this node? depth: usize, @@ -273,32 +312,17 @@ impl SizeNode { } impl DepthTree { - // My previous work to convert from a node tree to this tree-with-width dependend on the node tree - // being a recursive data structure. Now I need to find a way to convert a slab tree to this width - // tree. - // - // It all feels like a lot of custom weirdness. I shouldn't need a bunch of custom data structures, - // so I want to eliminate the "Tree" above and keep using the slab tree. I think I should be able - // to build these Node objects without needing a custom data structure. - fn new() -> Self { - Self(nary_tree::Tree::new()) - /* - Tree { - nodes: vec![Node { - id: 0, - node: root, - parent: None, - depth: 0, - width: RefCell::new(None), - children: vec![], - }], - } - */ - } - /* pub fn node(&self, idx: usize) -> &T { - &self.nodes[idx].node + &self.nodes[idx].content + } + + pub fn parent(&self, node: &Node) -> Option<&Node> { + if let Some(parent_idx) = node.parent { + self.nodes.get(parent_idx) + } else { + None + } } // Add a node to the parent specified by parent_idx. Return the new index. This cannot be used @@ -309,7 +333,7 @@ impl DepthTree { self.nodes.push(Node { id: next_idx, - node, + content: node, parent: Some(parent_idx), depth: parent.depth + 1, width: RefCell::new(None), @@ -328,7 +352,6 @@ impl DepthTree { .unwrap() .traverse_pre_order() .fold(0, |max, node| { - println!("node depth: {}", node.data().depth); if node.data().depth > max { node.data().depth } else { @@ -487,7 +510,7 @@ impl<'a> From<&'a GameTree> for DepthTree { Self(tree) } - None => Self::new(), + None => Self::default(), } } } @@ -529,7 +552,7 @@ pub struct BFSIter<'a, T> { } impl<'a, T> Iterator for BFSIter<'a, T> { - type Item = &'a T; + type Item = NodeRef<'a, T>; fn next(&mut self) -> Option { let retval = self.queue.pop_front(); @@ -538,7 +561,7 @@ impl<'a, T> Iterator for BFSIter<'a, T> { .children() .for_each(|noderef| self.queue.push_back(noderef)); } - retval.map(|retval| retval.data()) + retval } } @@ -630,7 +653,7 @@ mod test { ))) .node_id(); - let node_d = game_tree + let _node_d = game_tree .get_mut(node_c) .unwrap() .append(GameNode::MoveNode(MoveNode::new( @@ -639,7 +662,7 @@ mod test { ))) .node_id(); - let node_e = game_tree + let _node_e = game_tree .get_mut(node_c) .unwrap() .append(GameNode::MoveNode(MoveNode::new( @@ -648,7 +671,7 @@ mod test { ))) .node_id(); - let node_f = game_tree + let _node_f = game_tree .get_mut(node_c) .unwrap() .append(GameNode::MoveNode(MoveNode::new( @@ -657,7 +680,7 @@ mod test { ))) .node_id(); - let node_g = game_tree + let _node_g = game_tree .get_mut(node_a) .unwrap() .append(GameNode::MoveNode(MoveNode::new( @@ -732,66 +755,4 @@ mod test { assert_eq!(tree.position(test_tree.node_g), (1, 4)); */ } - - #[ignore] - #[test] - fn breadth_first_iter() { - /* - let mut node_a = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned())); - let mut node_b = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned())); - let mut node_c = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned())); - let node_d = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned())); - let node_e = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned())); - let node_f = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned())); - let node_g = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned())); - let mut node_h = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned())); - let node_i = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned())); - - let game = GameRecord::new( - GameType::Go, - Size { - width: 19, - height: 19, - }, - Player { - name: Some("Black".to_owned()), - rank: None, - team: None, - }, - Player { - name: Some("White".to_owned()), - rank: None, - team: None, - }, - ); - - node_c.children.push(GameNode::MoveNode(node_d.clone())); - node_c.children.push(GameNode::MoveNode(node_e.clone())); - node_c.children.push(GameNode::MoveNode(node_f.clone())); - - node_b.children.push(GameNode::MoveNode(node_c.clone())); - - node_h.children.push(GameNode::MoveNode(node_i.clone())); - - node_a.children.push(GameNode::MoveNode(node_b.clone())); - node_a.children.push(GameNode::MoveNode(node_g.clone())); - node_a.children.push(GameNode::MoveNode(node_h.clone())); - - let game_tree = GameNode::MoveNode(node_a.clone()); - - let tree = Tree::from(&game_tree); - - let mut iter = tree.bfs_iter(); - - assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_a.id)); - assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_b.id)); - assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_g.id)); - assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_h.id)); - assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_c.id)); - assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_i.id)); - assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_d.id)); - assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_e.id)); - assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_f.id)); - */ - } } diff --git a/otg/core/src/view_models/game_review.rs b/otg/core/src/view_models/game_review.rs new file mode 100644 index 0000000..a598b2a --- /dev/null +++ b/otg/core/src/view_models/game_review.rs @@ -0,0 +1,281 @@ +/* +Copyright 2024, Savanni D'Gerinel + +This file is part of On the Grid. + +On the Grid is free software: you can redistribute it and/or modify it under the terms of +the GNU General Public License as published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +On the Grid is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with On the Grid. If not, see . +*/ + +// Currenty my game review is able to show the current game and its tree. Now, I want to start +// tracking where in the tree that I am. This should be a combination of the abstract Tree and the +// gameTree. Chances are, if I just keep track of where I am in the abstract tree, I can find the +// relevant node in the game tree and then reproduce the line to get to that node. +// +// Moving through the game review tree shouldn't require a full invocatian. This object, and most +// other view models, should be exported to the UI. + +use crate::{types::SizeNode, DepthTree, Goban}; +use nary_tree::{NodeId, NodeRef}; +use sgf::{GameRecord, Player}; +use std::sync::{Arc, RwLock}; + +struct GameReviewViewModelPrivate { + // This is the ID of the current position in the game. The ID is specific to the GameRecord, + // not the ReviewTree. + current_position: Option, + game: GameRecord, + review_tree: DepthTree, +} + +#[derive(Clone)] +pub struct GameReviewViewModel { + inner: Arc>, +} + +impl GameReviewViewModel { + pub fn new(game: GameRecord) -> Self { + let (review_tree, current_position) = if !game.trees.is_empty() { + let review_tree = DepthTree::from(&game.trees[0]); + let current_position = game.mainline().unwrap().last().map(|nr| nr.node_id()); + (review_tree, current_position) + } else { + (DepthTree::default(), None) + }; + + Self { + inner: Arc::new(RwLock::new(GameReviewViewModelPrivate { + current_position, + game, + review_tree, + })), + } + } + + pub fn black_player(&self) -> Player { + self.inner.read().unwrap().game.black_player.clone() + } + + pub fn white_player(&self) -> Player { + self.inner.read().unwrap().game.white_player.clone() + } + + pub fn game_view(&self) -> Goban { + let inner = self.inner.read().unwrap(); + + let mut path: Vec = vec![]; + let mut current_id = inner.current_position; + while current_id.is_some() { + let current = current_id.unwrap(); + path.push(current); + current_id = inner.game.trees[0] + .get(current) + .unwrap() + .parent() + .map(|parent| parent.node_id()); + } + + path.reverse(); + Goban::default() + .apply_moves( + path.into_iter() + .map(|node_id| inner.game.trees[0].get(node_id).unwrap().data()), + ) + .unwrap() + + /* + if let Some(start) = inner.current_position { + let mut current_id = start; + let mut path = vec![current_id.clone()]; + + while let + /* + let mut current_node = inner.game.trees[0].get(current_position).unwrap(); + let mut path = vec![current_node.data()]; + while let Some(parent) = current_node.parent() { + path.push(parent.data()); + current_node = parent; + } + */ + + path.reverse(); + Goban::default().apply_moves(path).unwrap() + } else { + Goban::default() + } + */ + } + + pub fn map_tree(&self, f: F) + where + F: Fn(NodeRef<'_, SizeNode>, Option), + { + let inner = self.inner.read().unwrap(); + + for node in inner.review_tree.bfs_iter() { + f(node, inner.current_position); + } + } + + pub fn tree_max_depth(&self) -> usize { + self.inner.read().unwrap().review_tree.max_depth() + } + + // When moving forward on the tree, I grab the first child by default. I can then just advance + // the board state by applying the child. + pub fn next_move(&self) { + let mut inner = self.inner.write().unwrap(); + let current_position = inner.current_position.clone(); + match current_position { + Some(current_position) => { + let current_id = current_position.clone(); + let node = inner.game.trees[0].get(current_id).unwrap(); + if let Some(next_id) = node.first_child().map(|child| child.node_id()) { + inner.current_position = Some(next_id); + } + } + None => { + inner.current_position = inner.game.trees[0].root().map(|node| node.node_id()); + } + } + } + + // When moving backwards, I jump up to the parent. I'll then rebuild the board state from the + // root. + pub fn previous_move(&mut self) { + let mut inner = self.inner.write().unwrap(); + if let Some(current_position) = inner.current_position { + let current_node = inner.game.trees[0] + .get(current_position) + .expect("current_position should always correspond to a node in the tree"); + if let Some(parent_node) = current_node.parent() { + inner.current_position = Some(parent_node.node_id()); + } + } + } + + pub fn next_variant(&self) { + println!("move to the next variant amongst the options available"); + } + + pub fn previous_variant(&self) { + println!("move to the previous variant amongst the options available"); + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{Color, Coordinate}; + use std::path::Path; + + fn with_game_record(test: F) + where + F: FnOnce(GameReviewViewModel), + { + let records = sgf::parse_sgf_file(&Path::new("../../sgf/test_data/branch_test.sgf")) + .expect("to successfully load the test file"); + let record = records[0] + .as_ref() + .expect("to have successfully loaded the test record"); + + let view_model = GameReviewViewModel::new(record.clone()); + + test(view_model); + } + + #[test] + fn it_generates_a_mainline_board() { + with_game_record(|view| { + let goban = view.game_view(); + + for row in 0..18 { + for column in 0..18 { + if row == 3 && column == 3 { + assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::White)); + } else if row == 15 && column == 3 { + assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::White)); + } else if row == 3 && column == 15 { + assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::Black)); + } else if row == 15 && column == 14 { + assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::Black)); + } else { + assert_eq!( + goban.stone(&Coordinate { row, column }), + None, + "{} {}", + row, + column + ); + } + } + } + }); + } + + #[test] + fn it_moves_to_the_previous_mainline_move() { + with_game_record(|mut view| { + view.previous_move(); + let goban = view.game_view(); + + for row in 0..18 { + for column in 0..18 { + if row == 3 && column == 3 { + assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::White)); + } else if row == 3 && column == 15 { + assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::Black)); + } else if row == 15 && column == 14 { + assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::Black)); + } else { + assert_eq!( + goban.stone(&Coordinate { row, column }), + None, + "{} {}", + row, + column + ); + } + } + } + }); + } + + #[test] + fn it_moves_to_the_next_node() { + with_game_record(|mut view| { + view.previous_move(); + view.next_move(); + let goban = view.game_view(); + + for row in 0..18 { + for column in 0..18 { + if row == 3 && column == 3 { + assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::White)); + } else if row == 15 && column == 3 { + assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::White)); + } else if row == 3 && column == 15 { + assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::Black)); + } else if row == 15 && column == 14 { + assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::Black)); + } else { + assert_eq!( + goban.stone(&Coordinate { row, column }), + None, + "{} {}", + row, + column + ); + } + } + } + }); + } +} diff --git a/otg/core/src/view_models/mod.rs b/otg/core/src/view_models/mod.rs new file mode 100644 index 0000000..77317e7 --- /dev/null +++ b/otg/core/src/view_models/mod.rs @@ -0,0 +1,20 @@ +/* +Copyright 2024, Savanni D'Gerinel + +This file is part of On the Grid. + +On the Grid is free software: you can redistribute it and/or modify it under the terms of +the GNU General Public License as published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +On the Grid is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with On the Grid. If not, see . +*/ + + +mod game_review; +pub use game_review::GameReviewViewModel; + diff --git a/otg/gtk/src/app_window.rs b/otg/gtk/src/app_window.rs index b40c5e6..ca92ac6 100644 --- a/otg/gtk/src/app_window.rs +++ b/otg/gtk/src/app_window.rs @@ -17,12 +17,14 @@ You should have received a copy of the GNU General Public License along with On use crate::{CoreApi, ResourceManager}; use adw::prelude::*; +use glib::Propagation; +use gtk::{gdk::Key, EventControllerKey}; use otg_core::{ settings::{SettingsRequest, SettingsResponse}, - CoreRequest, CoreResponse, + CoreRequest, CoreResponse, GameReviewViewModel, }; use sgf::GameRecord; -use std::{rc::Rc, sync::{Arc, RwLock}}; +use std::sync::{Arc, RwLock}; use crate::views::{GameReview, HomeView, SettingsView}; @@ -91,13 +93,32 @@ impl AppWindow { pub fn open_game_review(&self, game_record: GameRecord) { let header = adw::HeaderBar::new(); - let game_review = GameReview::new(self.core.clone(), game_record, self.resources.clone()); + let game_review = GameReview::new( + GameReviewViewModel::new(game_record), + self.resources.clone(), + ); let layout = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .build(); layout.append(&header); - layout.append(&game_review); + layout.append(&game_review.widget()); + + // This controller ensures that navigational keypresses get sent to the game review so that + // they're not changing the cursor focus in the app. + let keypress_controller = EventControllerKey::new(); + keypress_controller.connect_key_pressed({ + move |s, key, _, _| { + println!("layout keypress: {}", key); + if s.forward(&game_review.widget()) { + Propagation::Stop + } else { + Propagation::Proceed + } + } + }); + + layout.add_controller(keypress_controller); let page = adw::NavigationPage::builder() .can_pop(true) diff --git a/otg/gtk/src/components/goban.rs b/otg/gtk/src/components/goban.rs index 85d52fe..fff1fa0 100644 --- a/otg/gtk/src/components/goban.rs +++ b/otg/gtk/src/components/goban.rs @@ -37,16 +37,10 @@ You should have received a copy of the GNU General Public License along with On use crate::{perftrace, Resource, ResourceManager}; -use gio::resources_lookup_data; use glib::Object; -use gtk::{ - gdk_pixbuf::{Colorspace, InterpType, Pixbuf}, - prelude::*, - subclass::prelude::*, -}; -use image::{io::Reader as ImageReader, ImageError}; +use gtk::{gdk_pixbuf::Pixbuf, prelude::*, subclass::prelude::*}; use otg_core::{Color, Coordinate}; -use std::{cell::RefCell, io::Cursor, rc::Rc}; +use std::{cell::RefCell, rc::Rc}; const WIDTH: i32 = 800; const HEIGHT: i32 = 800; @@ -107,17 +101,12 @@ impl Goban { s } - fn redraw(&self, ctx: &cairo::Context, width: i32, height: i32) { - println!("{} x {}", width, height); - /* - let background = load_pixbuf( - "/com/luminescent-dreams/otg-gtk/wood_texture.jpg", - false, - WIDTH + 40, - HEIGHT + 40, - ); - */ + pub fn set_board_state(&mut self, board_state: otg_core::Goban) { + *self.imp().board_state.borrow_mut() = board_state; + self.queue_draw(); + } + fn redraw(&self, ctx: &cairo::Context, width: i32, height: i32) { let background = self .imp() .resource_manager @@ -257,11 +246,11 @@ impl Pen { let (x_loc, y_loc) = self.stone_location(row, col); match color { Color::White => match self.white_stone { - Some(ref white_stone) => ctx.set_source_pixbuf(&white_stone, x_loc, y_loc), + Some(ref white_stone) => ctx.set_source_pixbuf(white_stone, x_loc, y_loc), None => ctx.set_source_rgb(0.9, 0.9, 0.9), }, Color::Black => match self.black_stone { - Some(ref black_stone) => ctx.set_source_pixbuf(&black_stone, x_loc, y_loc), + Some(ref black_stone) => ctx.set_source_pixbuf(black_stone, x_loc, y_loc), None => ctx.set_source_rgb(0.0, 0.0, 0.0), }, } @@ -309,34 +298,3 @@ impl Pen { ) } } - -fn load_pixbuf( - path: &str, - transparency: bool, - width: i32, - height: i32, -) -> Result, ImageError> { - let image_bytes = resources_lookup_data(path, gio::ResourceLookupFlags::NONE).unwrap(); - - let image = ImageReader::new(Cursor::new(image_bytes)) - .with_guessed_format() - .unwrap() - .decode(); - image.map(|image| { - let stride = if transparency { - image.to_rgba8().sample_layout().height_stride - } else { - image.to_rgb8().sample_layout().height_stride - }; - Pixbuf::from_bytes( - &glib::Bytes::from(image.as_bytes()), - Colorspace::Rgb, - transparency, - 8, - image.width() as i32, - image.height() as i32, - stride as i32, - ) - .scale_simple(width, height, InterpType::Nearest) - }) -} diff --git a/otg/gtk/src/components/review_tree.rs b/otg/gtk/src/components/review_tree.rs index c2297ff..3aff89a 100644 --- a/otg/gtk/src/components/review_tree.rs +++ b/otg/gtk/src/components/review_tree.rs @@ -15,50 +15,42 @@ You should have received a copy of the GNU General Public License along with On */ use cairo::Context; -use glib::Object; -use gtk::{prelude::*, subclass::prelude::*}; -use otg_core::DepthTree; -use sgf::GameRecord; -use std::{cell::RefCell, rc::Rc}; -use uuid::Uuid; +use gtk::prelude::*; +use otg_core::GameReviewViewModel; const WIDTH: i32 = 200; const HEIGHT: i32 = 800; -#[derive(Default)] -pub struct ReviewTreePrivate { - record: Rc>>, - tree: Rc>>, -} +const RADIUS: f64 = 7.5; +const HIGHLIGHT_WIDTH: f64 = 4.; +const SPACING: f64 = 30.; -#[glib::object_subclass] -impl ObjectSubclass for ReviewTreePrivate { - const NAME: &'static str = "ReviewTree"; - type Type = ReviewTree; - type ParentType = gtk::DrawingArea; -} +#[derive(Clone)] +pub struct ReviewTree { + widget: gtk::ScrolledWindow, + drawing_area: gtk::DrawingArea, -impl ObjectImpl for ReviewTreePrivate {} -impl WidgetImpl for ReviewTreePrivate {} -impl DrawingAreaImpl for ReviewTreePrivate {} - -glib::wrapper! { - pub struct ReviewTree(ObjectSubclass) @extends gtk::Widget, gtk::DrawingArea; + view: GameReviewViewModel, } impl ReviewTree { - pub fn new(record: GameRecord) -> Self { - let s: Self = Object::new(); + pub fn new(view: GameReviewViewModel) -> ReviewTree { + let drawing_area = gtk::DrawingArea::new(); + let widget = gtk::ScrolledWindow::builder().child(&drawing_area).build(); - // TODO: there can be more than one tree, especially in instructional files. Either unify - // them into a single tree in the GameTree, or draw all of them here. - *s.imp().tree.borrow_mut() = Some(DepthTree::from(&record.trees[0])); - *s.imp().record.borrow_mut() = Some(record); + widget.set_width_request(WIDTH); + widget.set_height_request(HEIGHT); - s.set_width_request(WIDTH); - s.set_height_request(HEIGHT); + // TODO: figure out the maximum width of the tree so that we can also set a width request + drawing_area.set_height_request(view.tree_max_depth() as i32 * SPACING as i32); - s.set_draw_func({ + let s = Self { + widget, + drawing_area, + view, + }; + + s.drawing_area.set_draw_func({ let s = s.clone(); move |_, ctx, width, height| { s.redraw(ctx, width, height); @@ -68,168 +60,63 @@ impl ReviewTree { s } - pub fn redraw(&self, ctx: &Context, _width: i32, _height: i32) { - let tree: &Option = &self.imp().tree.borrow(); - match tree { - Some(ref tree) => { - for node in tree.bfs_iter() { - // draw a circle given the coordinates of the nodes - // I don't know the indent. How do I keep track of that? Do I track the position of - // the parent? do I need to just make it more intrinsically a part of the position - // code? - ctx.set_source_rgb(0.7, 0.7, 0.7); - let (row, column) = node.position(); - let y = (row as f64) * 20. + 10.; - let x = (column as f64) * 20. + 10.; - ctx.arc(x, y, 5., 0., 2. * std::f64::consts::PI); - let _ = ctx.stroke(); - } + pub fn queue_draw(&self) { + self.drawing_area.queue_draw(); + } + + fn redraw(&self, ctx: &Context, _width: i32, _height: i32) { + #[allow(deprecated)] + let context = WidgetExt::style_context(&self.widget); + #[allow(deprecated)] + let foreground_color = context.lookup_color("sidebar_fg_color").unwrap(); + #[allow(deprecated)] + let accent_color = context.lookup_color("accent_color").unwrap(); + + self.view.map_tree(move |node, current| { + let parent = node.parent(); + ctx.set_source_rgb( + foreground_color.red().into(), + foreground_color.green().into(), + foreground_color.blue().into(), + ); + let (row, column) = node.data().position(); + let y = (row as f64) * SPACING + RADIUS * 2.; + let x = (column as f64) * SPACING + RADIUS * 2.; + ctx.arc(x, y, RADIUS, 0., 2. * std::f64::consts::PI); + let _ = ctx.fill(); + + if let Some(parent) = parent { + ctx.set_line_width(1.); + let (row, column) = parent.data().position(); + let py = (row as f64) * SPACING + RADIUS * 2.; + let px = (column as f64) * SPACING + RADIUS * 2.; + ctx.move_to(px, py); + ctx.line_to(x, y); + let _ = ctx.stroke(); } - None => { - // if there is no tree present, then there's nothing to draw! + + if current == Some(node.data().game_node_id) { + ctx.set_line_width(HIGHLIGHT_WIDTH); + ctx.set_source_rgb( + accent_color.red().into(), + accent_color.green().into(), + accent_color.blue().into(), + ); + ctx.arc( + x, + y, + RADIUS + HIGHLIGHT_WIDTH / 2., + 0., + 2. * std::f64::consts::PI, + ); + let _ = ctx.stroke(); + ctx.set_line_width(2.); } - } + }); + } + + pub fn widget(&self) -> gtk::Widget { + self.widget.clone().upcast::() } } -// https://llimllib.github.io/pymag-trees/ -// I want to take advantage of the Wetherell Shannon algorithm, but I want some variations. In -// their diagram, they got a tree that looks like this. -// -// O -// |\ -// O O -// |\ \ \ -// O O O O -// |\ |\ -// O O O O -// -// In the same circumstance, what I want is this: -// -// O-- -// | \ -// O O -// |\ |\ -// O O O O -// |\ -// O O -// -// In order to keep things from being overly smooshed, I want to ensure that if a branch overlaps -// with another branch, there is some extra drawing space. This might actually be similar to adding -// the principal that "A parent should be centered over its children". -// -// So, given a tree, I need to know how many children exist at each level. Then I build parents -// atop the children. At level 3, I have four children, and that happens to be the maximum width of -// the graph. -// -// A bottom-up traversal: -// - Figure out the number of nodes at each depth - -/* -struct Tree { - width: Vec, // the total width of the tree at each depth -} -*/ - -#[cfg(test)] -mod test { - use super::*; - use sgf::{Color, GameNode, Move, MoveNode}; - - #[test] - fn it_calculates_width_for_single_node() { - let node = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned()))); - - assert_eq!(node_width(&node), 1); - } - - #[test] - fn it_calculates_width_for_node_with_children() { - let mut node_a = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); - let node_b = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned()))); - let node_c = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned()))); - let node_d = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned()))); - - node_a.children.push(node_b); - node_a.children.push(node_c); - node_a.children.push(node_d); - - assert_eq!(node_width(&GameNode::MoveNode(node_a)), 3); - } - - // A - // B E - // C D - #[test] - fn it_calculates_width_with_one_deep_child() { - let mut node_a = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); - let mut node_b = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); - let node_c = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); - let node_d = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); - let node_e = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); - - node_b.children.push(GameNode::MoveNode(node_c)); - node_b.children.push(GameNode::MoveNode(node_d)); - assert_eq!(node_width(&GameNode::MoveNode(node_b.clone())), 2); - - node_a.children.push(GameNode::MoveNode(node_b)); - node_a.children.push(GameNode::MoveNode(node_e)); - assert_eq!(node_width(&GameNode::MoveNode(node_a)), 3); - } - - // A - // B G H - // C I - // D E F - #[test] - fn it_calculates_a_complex_tree() { - let mut node_a = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); - let mut node_b = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); - let mut node_c = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); - let node_d = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); - let node_e = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); - let node_f = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); - let node_g = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); - let mut node_h = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); - let node_i = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); - - node_c.children.push(GameNode::MoveNode(node_d)); - node_c.children.push(GameNode::MoveNode(node_e)); - node_c.children.push(GameNode::MoveNode(node_f)); - assert_eq!(node_width(&GameNode::MoveNode(node_c.clone())), 3); - - node_b.children.push(GameNode::MoveNode(node_c)); - assert_eq!(node_width(&GameNode::MoveNode(node_b.clone())), 3); - - node_h.children.push(GameNode::MoveNode(node_i)); - - node_a.children.push(GameNode::MoveNode(node_b)); - node_a.children.push(GameNode::MoveNode(node_g)); - node_a.children.push(GameNode::MoveNode(node_h)); - // This should be 4 if I were collapsing levels correctly, but it is 5 until I return to - // figure that step out. - assert_eq!(node_width(&GameNode::MoveNode(node_a.clone())), 5); - } - - #[test] - fn a_nodes_children_get_separate_columns() { - let mut node_a = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); - let node_b = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned()))); - let node_c = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned()))); - let node_d = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned()))); - - node_a.children.push(node_b.clone()); - node_a.children.push(node_c.clone()); - node_a.children.push(node_d.clone()); - - assert_eq!( - node_children_columns(&GameNode::MoveNode(node_a)), - vec![0, 1, 2] - ); - } - - #[test] - fn text_renderer() { - assert!(false); - } -} diff --git a/otg/gtk/src/lib.rs b/otg/gtk/src/lib.rs index 7fe1471..91c97f1 100644 --- a/otg/gtk/src/lib.rs +++ b/otg/gtk/src/lib.rs @@ -49,8 +49,8 @@ pub struct ResourceManager { resources: Rc>>, } -impl ResourceManager { - pub fn new() -> Self { +impl Default for ResourceManager { + fn default() -> Self { let mut resources = HashMap::new(); for (path, xres, yres, transparency) in [ @@ -88,7 +88,9 @@ impl ResourceManager { resources: Rc::new(RefCell::new(resources)), } } +} +impl ResourceManager { pub fn resource(&self, path: &str) -> Option { self.resources.borrow().get(path).cloned() } @@ -123,7 +125,6 @@ impl ResourceManager { .scale_simple(width, height, InterpType::Nearest) }) } - } pub fn perftrace(trace_name: &str, f: F) -> A diff --git a/otg/gtk/src/main.rs b/otg/gtk/src/main.rs index 6035d16..292ccdc 100644 --- a/otg/gtk/src/main.rs +++ b/otg/gtk/src/main.rs @@ -122,7 +122,7 @@ fn main() { app.connect_activate({ move |app| { - let resources = ResourceManager::new(); + let resources = ResourceManager::default(); let core_api = CoreApi { core: core.clone() }; let app_window = AppWindow::new(app, core_api, resources); diff --git a/otg/gtk/src/views/game_review.rs b/otg/gtk/src/views/game_review.rs index 101c53c..e799109 100644 --- a/otg/gtk/src/views/game_review.rs +++ b/otg/gtk/src/views/game_review.rs @@ -22,16 +22,21 @@ You should have received a copy of the GNU General Public License along with On // I'll get all of the information about the game from the core, and then render everything in the // UI. So this will be a heavy lift on the UI side. -use crate::{ - components::{Goban, PlayerCard, ReviewTree}, CoreApi, ResourceManager -}; -use glib::Object; -use gtk::{prelude::*, subclass::prelude::*}; -use otg_core::Color; -use sgf::GameRecord; +use std::{cell::RefCell, rc::Rc}; +use crate::{ + components::{Goban, PlayerCard, ReviewTree}, + ResourceManager, +}; +use glib::Propagation; +use gtk::{gdk::Key, prelude::*, EventControllerKey}; +use otg_core::{Color, GameReviewViewModel}; + +/* #[derive(Default)] -pub struct GameReviewPrivate {} +pub struct GameReviewPrivate { + model: Rc>>, +} #[glib::object_subclass] impl ObjectSubclass for GameReviewPrivate { @@ -52,14 +57,76 @@ impl GameReview { pub fn new(_api: CoreApi, record: GameRecord, resources: ResourceManager) -> Self { let s: Self = Object::builder().build(); + + s + } +} +*/ + +#[derive(Clone)] +pub struct GameReview { + widget: gtk::Box, + goban: Rc>>, + review_tree: Rc>>, + + resources: ResourceManager, + view: Rc>, +} + +impl GameReview { + pub fn new(view: GameReviewViewModel, resources: ResourceManager) -> Self { + let widget = gtk::Box::builder().build(); + + let view = Rc::new(RefCell::new(view)); + + let s = Self { + widget, + goban: Default::default(), + review_tree: Default::default(), + resources, + view, + }; + + let keypress_controller = EventControllerKey::new(); + keypress_controller.connect_key_pressed({ + let s = s.clone(); + move |_, key, _, _| { + let mut view = s.view.borrow_mut(); + match key { + Key::Down => view.next_move(), + Key::Up => view.previous_move(), + Key::Left => view.previous_variant(), + Key::Right => view.next_variant(), + _ => { + return Propagation::Proceed; + } + } + + match *s.goban.borrow_mut() { + Some(ref mut goban) => goban.set_board_state(view.game_view()), + None => {} + }; + match *s.review_tree.borrow() { + Some(ref tree) => tree.queue_draw(), + None => {} + } + Propagation::Stop + } + }); + + s.widget.add_controller(keypress_controller); + + s.render(); + + s + } + + fn render(&self) { // It's actually really bad to be just throwing away errors. Panics make everyone unhappy. // This is not a fatal error, so I'll replace this `unwrap` call with something that // renders the board and notifies the user of a problem that cannot be resolved. - let board_repr = match record.mainline() { - Some(iter) => otg_core::Goban::default().apply_moves(iter).unwrap(), - None => otg_core::Goban::default(), - }; - let board = Goban::new(board_repr, resources); + let board_repr = self.view.borrow().game_view(); + let board = Goban::new(board_repr, self.resources.clone()); /* s.attach(&board, 0, 0, 2, 2); @@ -76,7 +143,7 @@ impl GameReview { // The review tree needs to know the record for being able to render all of the nodes. Once // keyboard input is being handled, the tree will have to be updated on each keystroke in // order to show the user where they are within the game record. - let review_tree = ReviewTree::new(record.clone()); + let review_tree = ReviewTree::new(self.view.borrow().clone()); // I think most keyboard focus is going to end up being handled here in GameReview, as // keystrokes need to affect both the goban and the review tree simultanesouly. Possibly @@ -88,14 +155,24 @@ impl GameReview { .spacing(4) .build(); - player_information_section.append(&PlayerCard::new(Color::Black, &record.black_player)); - player_information_section.append(&PlayerCard::new(Color::White, &record.white_player)); + player_information_section + .append(&PlayerCard::new(Color::Black, &self.view.borrow().black_player())); + player_information_section + .append(&PlayerCard::new(Color::White, &self.view.borrow().white_player())); - s.append(&board); + self.widget.append(&board); sidebar.append(&player_information_section); - sidebar.append(&review_tree); - s.append(&sidebar); + sidebar.append(&review_tree.widget()); + self.widget.append(&sidebar); - s + *self.goban.borrow_mut() = Some(board); + *self.review_tree.borrow_mut() = Some(review_tree); + } + + fn redraw(&self) { + } + + pub fn widget(&self) -> gtk::Widget { + self.widget.clone().upcast::() } } diff --git a/sgf/src/game.rs b/sgf/src/game.rs index d21812a..adc751a 100644 --- a/sgf/src/game.rs +++ b/sgf/src/game.rs @@ -2,8 +2,8 @@ use crate::{ parser::{self, Annotation, Evaluation, Move, SetupInstr, Size, UnknownProperty}, Color, Date, GameResult, GameType, }; -use serde::{Deserialize, Serialize}; use nary_tree::{NodeId, NodeMut, NodeRef, Tree}; +use serde::{Deserialize, Serialize}; use std::{ collections::{HashMap, HashSet, VecDeque}, fmt, @@ -136,7 +136,7 @@ impl GameRecord { /// Generate a list of moves which constitute the main line of the game. This is the game as it /// was actually played out, and by convention consists of the first node in each list of /// children. - pub fn mainline(&self) -> Option> { + pub fn mainline(&self) -> Option>> { if !self.trees.is_empty() { Some(MainlineIter { next: self.trees[0].root(), @@ -405,7 +405,7 @@ pub struct MainlineIter<'a> { } impl<'a> Iterator for MainlineIter<'a> { - type Item = &'a GameNode; + type Item = NodeRef<'a, GameNode>; fn next(&mut self) -> Option { if let Some(next) = self.next.take() { @@ -413,7 +413,7 @@ impl<'a> Iterator for MainlineIter<'a> { self.next = next .first_child() .and_then(|child| self.tree.get(child.node_id())); - Some(ret.data()) + Some(ret) } else { None } @@ -634,6 +634,7 @@ mod test { assert_eq!(tree.nodes().len(), 0); } + /* #[test] fn it_can_add_moves_to_a_game() { let mut game = GameRecord::new( @@ -660,6 +661,7 @@ mod test { assert_eq!(nodes[1].id(), second_move.id); */ } +*/ #[ignore] #[test] @@ -820,6 +822,7 @@ mod path_test { let moves = game .mainline() .expect("there should be a mainline in this file") + .map(|nr| nr.data()) .collect::>(); assert_matches!(moves[0], GameNode::MoveNode(node) => { assert_eq!(node.color, Color::Black); @@ -845,6 +848,7 @@ mod path_test { let moves = game .mainline() .expect("there should be a mainline in this file") + .map(|nr| nr.data()) .collect::>(); assert_matches!(moves[1], GameNode::MoveNode(node) => { assert_eq!(node.color, Color::White); diff --git a/sgf/src/lib.rs b/sgf/src/lib.rs index 9fc15ca..b49a060 100644 --- a/sgf/src/lib.rs +++ b/sgf/src/lib.rs @@ -4,7 +4,7 @@ mod game; pub use game::{GameNode, GameRecord, GameTree, MoveNode, Player}; mod parser; -pub use parser::{parse_collection, Move}; +pub use parser::{parse_collection, Move, Size}; mod types; pub use types::*; diff --git a/sgf/src/parser.rs b/sgf/src/parser.rs index d395dd1..8492dd2 100644 --- a/sgf/src/parser.rs +++ b/sgf/src/parser.rs @@ -302,10 +302,10 @@ impl Move { Move::Move(s) => { if s.len() == 2 { let mut parts = s.chars(); - let row_char = parts.next().unwrap(); - let row = row_char as u8 - b'a'; let column_char = parts.next().unwrap(); let column = column_char as u8 - b'a'; + let row_char = parts.next().unwrap(); + let row = row_char as u8 - b'a'; Some((row, column)) } else { unimplemented!("moves must contain exactly two characters");