From 56d7ad074a053bae36b0e261e32ef1c2424ea35c Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 9 Apr 2024 17:03:41 -0400 Subject: [PATCH] Set up a view model for the game review and highlight current node --- otg/core/src/lib.rs | 2 + otg/core/src/types.rs | 26 +++---- otg/core/src/view_models/game_review.rs | 95 +++++++++++++++++++++++++ otg/core/src/view_models/mod.rs | 20 ++++++ otg/gtk/src/app_window.rs | 15 ++-- otg/gtk/src/components/review_tree.rs | 85 ++++++++-------------- otg/gtk/src/views/game_review.rs | 63 +++++++++++++--- 7 files changed, 222 insertions(+), 84 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 3168a04..d0a1a0a 100644 --- a/otg/core/src/lib.rs +++ b/otg/core/src/lib.rs @@ -31,3 +31,5 @@ pub mod settings; mod types; pub use types::{BoardError, Color, Config, ConfigOption, LibraryPath, Player, Rank, Size, Tree}; +mod view_models; +pub use view_models::GameReviewViewModel; diff --git a/otg/core/src/types.rs b/otg/core/src/types.rs index 09a0a0f..82eb22b 100644 --- a/otg/core/src/types.rs +++ b/otg/core/src/types.rs @@ -242,7 +242,7 @@ pub struct Tree { #[derive(Debug)] pub struct Node { pub id: usize, - node: T, + pub content: T, parent: Option, depth: usize, width: RefCell>, @@ -254,7 +254,7 @@ impl Tree { Tree { nodes: vec![Node { id: 0, - node: root, + content: root, parent: None, depth: 0, width: RefCell::new(None), @@ -264,7 +264,7 @@ impl Tree { } 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 @@ -275,7 +275,7 @@ impl Tree { self.nodes.push(Node { id: next_idx, - node, + content: node, parent: Some(parent_idx), depth: parent.depth + 1, width: RefCell::new(None), @@ -576,14 +576,14 @@ mod test { 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)); + assert_matches!(iter.next(), Some(Node { content: uuid, .. }) => assert_eq!(*uuid, node_a.id)); + assert_matches!(iter.next(), Some(Node { content: uuid, .. }) => assert_eq!(*uuid, node_b.id)); + assert_matches!(iter.next(), Some(Node { content: uuid, .. }) => assert_eq!(*uuid, node_g.id)); + assert_matches!(iter.next(), Some(Node { content: uuid, .. }) => assert_eq!(*uuid, node_h.id)); + assert_matches!(iter.next(), Some(Node { content: uuid, .. }) => assert_eq!(*uuid, node_c.id)); + assert_matches!(iter.next(), Some(Node { content: uuid, .. }) => assert_eq!(*uuid, node_i.id)); + assert_matches!(iter.next(), Some(Node { content: uuid, .. }) => assert_eq!(*uuid, node_d.id)); + assert_matches!(iter.next(), Some(Node { content: uuid, .. }) => assert_eq!(*uuid, node_e.id)); + assert_matches!(iter.next(), Some(Node { content: 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..2b164b6 --- /dev/null +++ b/otg/core/src/view_models/game_review.rs @@ -0,0 +1,95 @@ +/* +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::{BFSIter, Node}, Goban, Tree}; +use sgf::{GameRecord, Player}; +use std::sync::{Arc, RwLock}; +use uuid::Uuid; + +struct GameReviewViewModelPrivate { + current_position: Option, + game: GameRecord, + review_tree: Tree, +} + +#[derive(Clone)] +pub struct GameReviewViewModel { + imp: Arc>, +} + +impl GameReviewViewModel { + pub fn new(game: GameRecord) -> Self { + let review_tree = Tree::from(&game.children[0]); + let current_position = game.mainline().last().map(|node| node.id()); + + 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(); + + Goban::default().apply_moves(imp.game.mainline()).unwrap() + } + + pub fn map_tree(&self, f: F) + where + F: Fn(&Tree, &Node, Option), + { + let imp = self.imp.read().unwrap(); + + for node in imp.review_tree.bfs_iter() { + f(&imp.review_tree, 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..01bd6f2 100644 --- a/otg/gtk/src/app_window.rs +++ b/otg/gtk/src/app_window.rs @@ -18,11 +18,13 @@ use crate::{CoreApi, ResourceManager}; use adw::prelude::*; use otg_core::{ - settings::{SettingsRequest, SettingsResponse}, - CoreRequest, CoreResponse, + settings::{SettingsRequest, SettingsResponse}, CoreRequest, CoreResponse, GameReviewViewModel }; use sgf::GameRecord; -use std::{rc::Rc, sync::{Arc, RwLock}}; +use std::{ + rc::Rc, + sync::{Arc, RwLock}, +}; use crate::views::{GameReview, HomeView, SettingsView}; @@ -91,13 +93,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/review_tree.rs b/otg/gtk/src/components/review_tree.rs index f759a34..9f3b13e 100644 --- a/otg/gtk/src/components/review_tree.rs +++ b/otg/gtk/src/components/review_tree.rs @@ -17,46 +17,28 @@ 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::Tree; -use sgf::GameRecord; -use std::{cell::RefCell, rc::Rc}; -use uuid::Uuid; +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(); - *s.imp().tree.borrow_mut() = Some(Tree::from(&record.children[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); @@ -66,27 +48,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) = tree.position(node.id); - 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 |tree, node, current| { + ctx.set_source_rgb(0.7, 0.7, 0.7); + let (row, column) = tree.position(node.id); + 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.content) { + 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::() } } @@ -123,12 +104,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/views/game_review.rs b/otg/gtk/src/views/game_review.rs index 721c5d9..82dd95e 100644 --- a/otg/gtk/src/views/game_review.rs +++ b/otg/gtk/src/views/game_review.rs @@ -23,15 +23,20 @@ You should have received a copy of the GNU General Public License along with On // UI. So this will be a heavy lift on the UI side. use crate::{ - components::{Goban, PlayerCard, ReviewTree}, CoreApi, ResourceManager + components::{Goban, PlayerCard, ReviewTree}, + CoreApi, ResourceManager, }; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; -use otg_core::Color; +use otg_core::{Color, GameReviewViewModel}; use sgf::GameRecord; +use std::{cell::RefCell, rc::Rc}; +/* #[derive(Default)] -pub struct GameReviewPrivate {} +pub struct GameReviewPrivate { + model: Rc>>, +} #[glib::object_subclass] impl ObjectSubclass for GameReviewPrivate { @@ -52,13 +57,45 @@ 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 = otg_core::Goban::default() .apply_moves(record.mainline()) .unwrap(); - 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); @@ -75,7 +112,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 @@ -87,14 +124,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::() } }