From 4f8a1636c15e4831c897be2ef596a9799f11a9cc Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 9 Apr 2024 17:03:41 -0400 Subject: [PATCH 1/6] Set up a view model for the game review and highlight current node --- otg/core/src/lib.rs | 3 + otg/core/src/types.rs | 102 +++-------------------- otg/core/src/view_models/game_review.rs | 105 ++++++++++++++++++++++++ otg/core/src/view_models/mod.rs | 20 +++++ otg/gtk/src/app_window.rs | 11 ++- otg/gtk/src/components/goban.rs | 45 +--------- otg/gtk/src/components/review_tree.rs | 90 +++++++------------- otg/gtk/src/lib.rs | 7 +- otg/gtk/src/main.rs | 2 +- otg/gtk/src/views/game_review.rs | 69 +++++++++++----- sgf/src/game.rs | 12 ++- 11 files changed, 244 insertions(+), 222 deletions(-) create mode 100644 otg/core/src/view_models/game_review.rs create mode 100644 otg/core/src/view_models/mod.rs 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..cc9147b 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::{ @@ -252,6 +253,12 @@ 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 @@ -273,32 +280,9 @@ 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 } // Add a node to the parent specified by parent_idx. Return the new index. This cannot be used @@ -309,7 +293,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), @@ -487,7 +471,7 @@ impl<'a> From<&'a GameTree> for DepthTree { Self(tree) } - None => Self::new(), + None => Self::default(), } } } @@ -529,7 +513,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 +522,7 @@ impl<'a, T> Iterator for BFSIter<'a, T> { .children() .for_each(|noderef| self.queue.push_back(noderef)); } - retval.map(|retval| retval.data()) + retval } } @@ -732,66 +716,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..46f2675 --- /dev/null +++ b/otg/core/src/view_models/game_review.rs @@ -0,0 +1,105 @@ +/* +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 { + current_position: Option, + game: GameRecord, + review_tree: DepthTree, +} + +#[derive(Clone)] +pub struct GameReviewViewModel { + imp: 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 { + imp: Arc::new(RwLock::new(GameReviewViewModelPrivate { + current_position, + game, + review_tree, + })), + } + } + + pub fn black_player(&self) -> Player { + self.imp.read().unwrap().game.black_player.clone() + } + + pub fn white_player(&self) -> Player { + self.imp.read().unwrap().game.white_player.clone() + } + + pub fn game_view(&self) -> Goban { + let imp = self.imp.read().unwrap(); + let mainline = imp.game.mainline(); + match mainline { + Some(mainline) => Goban::default() + .apply_moves(mainline.map(|nr| nr.data())) + .unwrap(), + None => Goban::default(), + } + } + + pub fn map_tree(&self, f: F) + where + F: Fn(NodeRef<'_, SizeNode>, Option), + { + let imp = self.imp.read().unwrap(); + + for node in imp.review_tree.bfs_iter() { + f(node, imp.current_position); + } + } + + pub fn move_forward(&self) { + unimplemented!() + } + + pub fn move_backward(&self) { + unimplemented!() + } + + pub fn next_variant(&self) { + unimplemented!() + } + + pub fn previous_variant(&self) { + unimplemented!() + } +} 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..8526cd6 100644 --- a/otg/gtk/src/app_window.rs +++ b/otg/gtk/src/app_window.rs @@ -19,10 +19,10 @@ use adw::prelude::*; 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 +91,16 @@ 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()); 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..5b2e524 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; @@ -257,11 +251,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 +303,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..420dcdc 100644 --- a/otg/gtk/src/components/review_tree.rs +++ b/otg/gtk/src/components/review_tree.rs @@ -15,50 +15,29 @@ 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>>, -} +#[derive(Clone)] +pub struct ReviewTree { + widget: gtk::DrawingArea, -#[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) @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 widget = gtk::DrawingArea::new(); - // 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); + let s = Self { widget, view }; - s.set_draw_func({ + s.widget.set_draw_func({ let s = s.clone(); move |_, ctx, width, height| { s.redraw(ctx, width, height); @@ -68,27 +47,26 @@ 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(); - } + fn redraw(&self, ctx: &Context, _width: i32, _height: i32) { + self.view.map_tree(move |node, current| { + ctx.set_source_rgb(0.7, 0.7, 0.7); + let (row, column) = node.data().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); + + if current == Some(node.node_id()) { + ctx.set_line_width(3.); + } else { + ctx.set_line_width(1.); } - None => { - // if there is no tree present, then there's nothing to draw! - } - } + + let _ = ctx.stroke(); + }); + } + + pub fn widget(&self) -> gtk::Widget { + self.widget.clone().upcast::() } } @@ -125,12 +103,6 @@ impl ReviewTree { // 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::*; 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..510e586 100644 --- a/otg/gtk/src/views/game_review.rs +++ b/otg/gtk/src/views/game_review.rs @@ -22,16 +22,15 @@ 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 crate::{components::{Goban, PlayerCard, ReviewTree}, ResourceManager}; +use gtk::{prelude::*}; +use otg_core::{Color, GameReviewViewModel}; +/* #[derive(Default)] -pub struct GameReviewPrivate {} +pub struct GameReviewPrivate { + model: Rc>>, +} #[glib::object_subclass] impl ObjectSubclass for GameReviewPrivate { @@ -52,14 +51,40 @@ impl GameReview { pub fn new(_api: CoreApi, record: GameRecord, resources: ResourceManager) -> Self { let s: Self = Object::builder().build(); + + s + } +} +*/ + +pub struct GameReview { + widget: gtk::Box, + + resources: ResourceManager, + view: GameReviewViewModel, +} + +impl GameReview { + pub fn new(view: GameReviewViewModel, resources: ResourceManager) -> Self { + let widget = gtk::Box::builder().build(); + + let s = Self { + widget, + resources, + view, + }; + + 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.game_view(); + let board = Goban::new(board_repr, self.resources.clone()); /* s.attach(&board, 0, 0, 2, 2); @@ -76,7 +101,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.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 +113,18 @@ 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.black_player())); + player_information_section + .append(&PlayerCard::new(Color::White, &self.view.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 + 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); -- 2.44.1 From 6165d659770e33070ad4538bcc90122527a042bf Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 9 Apr 2024 17:46:06 -0400 Subject: [PATCH 2/6] Make the review tree scrollable --- otg/core/src/view_models/game_review.rs | 4 ++++ otg/gtk/src/components/review_tree.rs | 14 ++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/otg/core/src/view_models/game_review.rs b/otg/core/src/view_models/game_review.rs index 46f2675..7cdf068 100644 --- a/otg/core/src/view_models/game_review.rs +++ b/otg/core/src/view_models/game_review.rs @@ -87,6 +87,10 @@ impl GameReviewViewModel { } } + pub fn tree_max_depth(&self) -> usize { + self.imp.read().unwrap().review_tree.max_depth() + } + pub fn move_forward(&self) { unimplemented!() } diff --git a/otg/gtk/src/components/review_tree.rs b/otg/gtk/src/components/review_tree.rs index 420dcdc..dbd76d9 100644 --- a/otg/gtk/src/components/review_tree.rs +++ b/otg/gtk/src/components/review_tree.rs @@ -23,21 +23,27 @@ const HEIGHT: i32 = 800; #[derive(Clone)] pub struct ReviewTree { - widget: gtk::DrawingArea, + widget: gtk::ScrolledWindow, + drawing_area: gtk::DrawingArea, view: GameReviewViewModel, } impl ReviewTree { pub fn new(view: GameReviewViewModel) -> ReviewTree { - let widget = gtk::DrawingArea::new(); + let drawing_area = gtk::DrawingArea::new(); + let widget = gtk::ScrolledWindow::builder() + .child(&drawing_area) + .build(); widget.set_width_request(WIDTH); widget.set_height_request(HEIGHT); - let s = Self { widget, view }; + drawing_area.set_height_request(view.tree_max_depth() as i32 * 20 + 40); - s.widget.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); -- 2.44.1 From 9540a2c5bba4680eb56ef88d9bf1a537c5bd065a Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 9 Apr 2024 21:28:34 -0400 Subject: [PATCH 3/6] Highlight the current node and make all nodes a bit larger --- otg/core/src/types.rs | 11 +++- otg/gtk/src/components/review_tree.rs | 72 +++++++++++++++++++++------ 2 files changed, 66 insertions(+), 17 deletions(-) diff --git a/otg/core/src/types.rs b/otg/core/src/types.rs index cc9147b..6ba99e9 100644 --- a/otg/core/src/types.rs +++ b/otg/core/src/types.rs @@ -263,8 +263,7 @@ impl Default for DepthTree { 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, @@ -285,6 +284,14 @@ impl DepthTree { &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 // to add the root node, but the constructor should handle that, anyway. fn add_node(&mut self, parent_idx: usize, node: T) -> usize { diff --git a/otg/gtk/src/components/review_tree.rs b/otg/gtk/src/components/review_tree.rs index dbd76d9..aa02952 100644 --- a/otg/gtk/src/components/review_tree.rs +++ b/otg/gtk/src/components/review_tree.rs @@ -21,6 +21,10 @@ use otg_core::GameReviewViewModel; const WIDTH: i32 = 200; const HEIGHT: i32 = 800; +const RADIUS: f64 = 7.5; +const HIGHLIGHT_WIDTH: f64 = 4.; +const SPACING: f64 = 30.; + #[derive(Clone)] pub struct ReviewTree { widget: gtk::ScrolledWindow, @@ -32,16 +36,19 @@ pub struct ReviewTree { impl ReviewTree { pub fn new(view: GameReviewViewModel) -> ReviewTree { let drawing_area = gtk::DrawingArea::new(); - let widget = gtk::ScrolledWindow::builder() - .child(&drawing_area) - .build(); + let widget = gtk::ScrolledWindow::builder().child(&drawing_area).build(); widget.set_width_request(WIDTH); widget.set_height_request(HEIGHT); - drawing_area.set_height_request(view.tree_max_depth() as i32 * 20 + 40); + // 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); - let s = Self { widget, drawing_area, view }; + let s = Self { + widget, + drawing_area, + view, + }; s.drawing_area.set_draw_func({ let s = s.clone(); @@ -54,20 +61,55 @@ impl ReviewTree { } fn redraw(&self, ctx: &Context, _width: i32, _height: i32) { - self.view.map_tree(move |node, current| { - ctx.set_source_rgb(0.7, 0.7, 0.7); - let (row, column) = node.data().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); + println!("redraw: {} {}", _width, _height); - if current == Some(node.node_id()) { - ctx.set_line_width(3.); - } else { + #[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(); } - let _ = ctx.stroke(); + 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.); + } }); } -- 2.44.1 From cbfb3f2e37d67ce346d83e5efcf1eb6a90ad3687 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Wed, 1 May 2024 09:36:48 -0400 Subject: [PATCH 4/6] Clean up tests --- otg/core/src/types.rs | 41 +++++++- otg/gtk/src/components/review_tree.rs | 135 -------------------------- 2 files changed, 37 insertions(+), 139 deletions(-) diff --git a/otg/core/src/types.rs b/otg/core/src/types.rs index 6ba99e9..5b9943a 100644 --- a/otg/core/src/types.rs +++ b/otg/core/src/types.rs @@ -243,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 { @@ -621,7 +654,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( @@ -630,7 +663,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( @@ -639,7 +672,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( @@ -648,7 +681,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( diff --git a/otg/gtk/src/components/review_tree.rs b/otg/gtk/src/components/review_tree.rs index aa02952..b6d4880 100644 --- a/otg/gtk/src/components/review_tree.rs +++ b/otg/gtk/src/components/review_tree.rs @@ -118,138 +118,3 @@ impl ReviewTree { } } -// 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 - -#[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); - } -} -- 2.44.1 From 7dd531b4931dcad9693a330fad7a2e2d6b30f328 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 7 May 2024 07:53:15 -0400 Subject: [PATCH 5/6] It is now possible to move backwards and forwards on the mainline of a tree --- otg/core/src/goban.rs | 2 +- otg/core/src/view_models/game_review.rs | 214 +++++++++++++++++++++--- otg/gtk/src/app_window.rs | 18 ++ otg/gtk/src/components/goban.rs | 6 + otg/gtk/src/views/game_review.rs | 56 ++++++- sgf/src/lib.rs | 2 +- sgf/src/parser.rs | 5 +- 7 files changed, 271 insertions(+), 32 deletions(-) diff --git a/otg/core/src/goban.rs b/otg/core/src/goban.rs index edff8b1..04d659b 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, diff --git a/otg/core/src/view_models/game_review.rs b/otg/core/src/view_models/game_review.rs index 7cdf068..a598b2a 100644 --- a/otg/core/src/view_models/game_review.rs +++ b/otg/core/src/view_models/game_review.rs @@ -28,6 +28,8 @@ 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, @@ -35,7 +37,7 @@ struct GameReviewViewModelPrivate { #[derive(Clone)] pub struct GameReviewViewModel { - imp: Arc>, + inner: Arc>, } impl GameReviewViewModel { @@ -49,7 +51,7 @@ impl GameReviewViewModel { }; Self { - imp: Arc::new(RwLock::new(GameReviewViewModelPrivate { + inner: Arc::new(RwLock::new(GameReviewViewModelPrivate { current_position, game, review_tree, @@ -58,52 +60,222 @@ impl GameReviewViewModel { } pub fn black_player(&self) -> Player { - self.imp.read().unwrap().game.black_player.clone() + self.inner.read().unwrap().game.black_player.clone() } pub fn white_player(&self) -> Player { - self.imp.read().unwrap().game.white_player.clone() + self.inner.read().unwrap().game.white_player.clone() } pub fn game_view(&self) -> Goban { - let imp = self.imp.read().unwrap(); - let mainline = imp.game.mainline(); - match mainline { - Some(mainline) => Goban::default() - .apply_moves(mainline.map(|nr| nr.data())) - .unwrap(), - None => Goban::default(), + 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 imp = self.imp.read().unwrap(); + let inner = self.inner.read().unwrap(); - for node in imp.review_tree.bfs_iter() { - f(node, imp.current_position); + for node in inner.review_tree.bfs_iter() { + f(node, inner.current_position); } } pub fn tree_max_depth(&self) -> usize { - self.imp.read().unwrap().review_tree.max_depth() + self.inner.read().unwrap().review_tree.max_depth() } - pub fn move_forward(&self) { - unimplemented!() + // 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()); + } + } } - pub fn move_backward(&self) { - unimplemented!() + // 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) { - unimplemented!() + println!("move to the next variant amongst the options available"); } pub fn previous_variant(&self) { - unimplemented!() + 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/gtk/src/app_window.rs b/otg/gtk/src/app_window.rs index 8526cd6..ca92ac6 100644 --- a/otg/gtk/src/app_window.rs +++ b/otg/gtk/src/app_window.rs @@ -17,6 +17,8 @@ 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, GameReviewViewModel, @@ -102,6 +104,22 @@ impl AppWindow { layout.append(&header); 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) .title("Game Review") diff --git a/otg/gtk/src/components/goban.rs b/otg/gtk/src/components/goban.rs index 5b2e524..a68b2d2 100644 --- a/otg/gtk/src/components/goban.rs +++ b/otg/gtk/src/components/goban.rs @@ -101,6 +101,12 @@ impl Goban { s } + pub fn set_board_state(&mut self, board_state: otg_core::Goban) { + println!("updating board state"); + *self.imp().board_state.borrow_mut() = board_state; + self.queue_draw(); + } + fn redraw(&self, ctx: &cairo::Context, width: i32, height: i32) { println!("{} x {}", width, height); /* diff --git a/otg/gtk/src/views/game_review.rs b/otg/gtk/src/views/game_review.rs index 510e586..54f6e86 100644 --- a/otg/gtk/src/views/game_review.rs +++ b/otg/gtk/src/views/game_review.rs @@ -22,8 +22,14 @@ 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}, ResourceManager}; -use gtk::{prelude::*}; +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}; /* @@ -57,23 +63,54 @@ impl GameReview { } */ +#[derive(Clone)] pub struct GameReview { widget: gtk::Box, + goban: Rc>>, resources: ResourceManager, - view: GameReviewViewModel, + 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: Rc::new(RefCell::new(None)), resources, view, }; + let keypress_controller = EventControllerKey::new(); + keypress_controller.connect_key_pressed({ + let s = s.clone(); + move |_, key, _, _| { + println!("keystroke: {}", 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 => {} + }; + Propagation::Stop + } + }); + + s.widget.add_controller(keypress_controller); + s.render(); s @@ -83,7 +120,7 @@ impl GameReview { // 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 = self.view.game_view(); + let board_repr = self.view.borrow().game_view(); let board = Goban::new(board_repr, self.resources.clone()); /* @@ -101,7 +138,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(self.view.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 @@ -114,14 +151,19 @@ impl GameReview { .build(); player_information_section - .append(&PlayerCard::new(Color::Black, &self.view.black_player())); + .append(&PlayerCard::new(Color::Black, &self.view.borrow().black_player())); player_information_section - .append(&PlayerCard::new(Color::White, &self.view.white_player())); + .append(&PlayerCard::new(Color::White, &self.view.borrow().white_player())); self.widget.append(&board); sidebar.append(&player_information_section); sidebar.append(&review_tree.widget()); self.widget.append(&sidebar); + + *self.goban.borrow_mut() = Some(board); + } + + fn redraw(&self) { } pub fn widget(&self) -> gtk::Widget { 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..b3dbd25 100644 --- a/sgf/src/parser.rs +++ b/sgf/src/parser.rs @@ -302,10 +302,11 @@ 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'; + println!("[{}] {} [{}] {}", row_char, row, column_char, column); Some((row, column)) } else { unimplemented!("moves must contain exactly two characters"); -- 2.44.1 From 15c4ae9bad74d467825ab413d0cfc3e1fcdac323 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 7 May 2024 08:49:49 -0400 Subject: [PATCH 6/6] Update the review tree when navigating --- otg/core/src/api.rs | 2 -- otg/core/src/goban.rs | 1 - otg/core/src/types.rs | 1 - otg/gtk/src/components/goban.rs | 11 ----------- otg/gtk/src/components/review_tree.rs | 6 ++++-- otg/gtk/src/views/game_review.rs | 10 ++++++++-- sgf/src/parser.rs | 1 - 7 files changed, 12 insertions(+), 20 deletions(-) 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 04d659b..ab0d0d8 100644 --- a/otg/core/src/goban.rs +++ b/otg/core/src/goban.rs @@ -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/types.rs b/otg/core/src/types.rs index 5b9943a..b2dc076 100644 --- a/otg/core/src/types.rs +++ b/otg/core/src/types.rs @@ -352,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 { diff --git a/otg/gtk/src/components/goban.rs b/otg/gtk/src/components/goban.rs index a68b2d2..fff1fa0 100644 --- a/otg/gtk/src/components/goban.rs +++ b/otg/gtk/src/components/goban.rs @@ -102,22 +102,11 @@ impl Goban { } pub fn set_board_state(&mut self, board_state: otg_core::Goban) { - println!("updating board state"); *self.imp().board_state.borrow_mut() = board_state; self.queue_draw(); } 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, - ); - */ - let background = self .imp() .resource_manager diff --git a/otg/gtk/src/components/review_tree.rs b/otg/gtk/src/components/review_tree.rs index b6d4880..3aff89a 100644 --- a/otg/gtk/src/components/review_tree.rs +++ b/otg/gtk/src/components/review_tree.rs @@ -60,9 +60,11 @@ impl ReviewTree { s } - fn redraw(&self, ctx: &Context, _width: i32, _height: i32) { - println!("redraw: {} {}", _width, _height); + 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)] diff --git a/otg/gtk/src/views/game_review.rs b/otg/gtk/src/views/game_review.rs index 54f6e86..e799109 100644 --- a/otg/gtk/src/views/game_review.rs +++ b/otg/gtk/src/views/game_review.rs @@ -67,6 +67,7 @@ impl GameReview { pub struct GameReview { widget: gtk::Box, goban: Rc>>, + review_tree: Rc>>, resources: ResourceManager, view: Rc>, @@ -80,7 +81,8 @@ impl GameReview { let s = Self { widget, - goban: Rc::new(RefCell::new(None)), + goban: Default::default(), + review_tree: Default::default(), resources, view, }; @@ -89,7 +91,6 @@ impl GameReview { keypress_controller.connect_key_pressed({ let s = s.clone(); move |_, key, _, _| { - println!("keystroke: {}", key); let mut view = s.view.borrow_mut(); match key { Key::Down => view.next_move(), @@ -105,6 +106,10 @@ impl GameReview { 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 } }); @@ -161,6 +166,7 @@ impl GameReview { self.widget.append(&sidebar); *self.goban.borrow_mut() = Some(board); + *self.review_tree.borrow_mut() = Some(review_tree); } fn redraw(&self) { diff --git a/sgf/src/parser.rs b/sgf/src/parser.rs index b3dbd25..8492dd2 100644 --- a/sgf/src/parser.rs +++ b/sgf/src/parser.rs @@ -306,7 +306,6 @@ impl Move { let column = column_char as u8 - b'a'; let row_char = parts.next().unwrap(); let row = row_char as u8 - b'a'; - println!("[{}] {} [{}] {}", row_char, row, column_char, column); Some((row, column)) } else { unimplemented!("moves must contain exactly two characters"); -- 2.44.1