Compare commits

...

3 Commits

7 changed files with 281 additions and 84 deletions

View File

@ -31,3 +31,5 @@ pub mod settings;
mod types; mod types;
pub use types::{BoardError, Color, Config, ConfigOption, LibraryPath, Player, Rank, Size, Tree}; pub use types::{BoardError, Color, Config, ConfigOption, LibraryPath, Player, Rank, Size, Tree};
mod view_models;
pub use view_models::GameReviewViewModel;

View File

@ -242,7 +242,7 @@ pub struct Tree<T> {
#[derive(Debug)] #[derive(Debug)]
pub struct Node<T> { pub struct Node<T> {
pub id: usize, pub id: usize,
node: T, pub content: T,
parent: Option<usize>, parent: Option<usize>,
depth: usize, depth: usize,
width: RefCell<Option<usize>>, width: RefCell<Option<usize>>,
@ -254,7 +254,7 @@ impl<T> Tree<T> {
Tree { Tree {
nodes: vec![Node { nodes: vec![Node {
id: 0, id: 0,
node: root, content: root,
parent: None, parent: None,
depth: 0, depth: 0,
width: RefCell::new(None), width: RefCell::new(None),
@ -264,7 +264,15 @@ impl<T> Tree<T> {
} }
pub fn node(&self, idx: usize) -> &T { pub fn node(&self, idx: usize) -> &T {
&self.nodes[idx].node &self.nodes[idx].content
}
pub fn parent(&self, node: &Node<T>) -> Option<&Node<T>> {
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 // Add a node to the parent specified by parent_idx. Return the new index. This cannot be used
@ -275,7 +283,7 @@ impl<T> Tree<T> {
self.nodes.push(Node { self.nodes.push(Node {
id: next_idx, id: next_idx,
node, content: node,
parent: Some(parent_idx), parent: Some(parent_idx),
depth: parent.depth + 1, depth: parent.depth + 1,
width: RefCell::new(None), width: RefCell::new(None),
@ -576,14 +584,14 @@ mod test {
let mut iter = tree.bfs_iter(); 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 { content: 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 { content: 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 { content: 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 { content: 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 { content: 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 { content: 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 { content: 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 { content: 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_f.id));
} }
} }

View File

@ -0,0 +1,99 @@
/*
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
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 <https://www.gnu.org/licenses/>.
*/
// 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<Uuid>,
game: GameRecord,
review_tree: Tree<Uuid>,
}
#[derive(Clone)]
pub struct GameReviewViewModel {
imp: Arc<RwLock<GameReviewViewModelPrivate>>,
}
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<F>(&self, f: F)
where
F: Fn(&Tree<Uuid>, &Node<Uuid>, Option<Uuid>),
{
let imp = self.imp.read().unwrap();
for node in imp.review_tree.bfs_iter() {
f(&imp.review_tree, node, imp.current_position);
}
}
pub fn tree_max_depth(&self) -> usize {
self.imp.read().unwrap().review_tree.max_depth()
}
pub fn move_forward(&self) {
unimplemented!()
}
pub fn move_backward(&self) {
unimplemented!()
}
pub fn next_variant(&self) {
unimplemented!()
}
pub fn previous_variant(&self) {
unimplemented!()
}
}

View File

@ -0,0 +1,20 @@
/*
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
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 <https://www.gnu.org/licenses/>.
*/
mod game_review;
pub use game_review::GameReviewViewModel;

View File

@ -18,11 +18,13 @@ use crate::{CoreApi, ResourceManager};
use adw::prelude::*; use adw::prelude::*;
use otg_core::{ use otg_core::{
settings::{SettingsRequest, SettingsResponse}, settings::{SettingsRequest, SettingsResponse}, CoreRequest, CoreResponse, GameReviewViewModel
CoreRequest, CoreResponse,
}; };
use sgf::GameRecord; use sgf::GameRecord;
use std::{rc::Rc, sync::{Arc, RwLock}}; use std::{
rc::Rc,
sync::{Arc, RwLock},
};
use crate::views::{GameReview, HomeView, SettingsView}; use crate::views::{GameReview, HomeView, SettingsView};
@ -91,13 +93,16 @@ impl AppWindow {
pub fn open_game_review(&self, game_record: GameRecord) { pub fn open_game_review(&self, game_record: GameRecord) {
let header = adw::HeaderBar::new(); 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() let layout = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical) .orientation(gtk::Orientation::Vertical)
.build(); .build();
layout.append(&header); layout.append(&header);
layout.append(&game_review); layout.append(&game_review.widget());
let page = adw::NavigationPage::builder() let page = adw::NavigationPage::builder()
.can_pop(true) .can_pop(true)

View File

@ -15,48 +15,42 @@ You should have received a copy of the GNU General Public License along with On
*/ */
use cairo::Context; use cairo::Context;
use glib::Object; use gtk::prelude::*;
use gtk::{prelude::*, subclass::prelude::*}; use otg_core::GameReviewViewModel;
use otg_core::Tree;
use sgf::GameRecord;
use std::{cell::RefCell, rc::Rc};
use uuid::Uuid;
const WIDTH: i32 = 200; const WIDTH: i32 = 200;
const HEIGHT: i32 = 800; const HEIGHT: i32 = 800;
#[derive(Default)] const RADIUS: f64 = 7.5;
pub struct ReviewTreePrivate { const HIGHLIGHT_WIDTH: f64 = 4.;
record: Rc<RefCell<Option<GameRecord>>>, const SPACING: f64 = 30.;
tree: Rc<RefCell<Option<Tree<Uuid>>>>,
}
#[glib::object_subclass] #[derive(Clone)]
impl ObjectSubclass for ReviewTreePrivate { pub struct ReviewTree {
const NAME: &'static str = "ReviewTree"; widget: gtk::ScrolledWindow,
type Type = ReviewTree; drawing_area: gtk::DrawingArea,
type ParentType = gtk::DrawingArea;
}
impl ObjectImpl for ReviewTreePrivate {} view: GameReviewViewModel,
impl WidgetImpl for ReviewTreePrivate {}
impl DrawingAreaImpl for ReviewTreePrivate {}
glib::wrapper! {
pub struct ReviewTree(ObjectSubclass<ReviewTreePrivate>) @extends gtk::Widget, gtk::DrawingArea;
} }
impl ReviewTree { impl ReviewTree {
pub fn new(record: GameRecord) -> Self { pub fn new(view: GameReviewViewModel) -> ReviewTree {
let s: Self = Object::new(); let drawing_area = gtk::DrawingArea::new();
let widget = gtk::ScrolledWindow::builder().child(&drawing_area).build();
*s.imp().tree.borrow_mut() = Some(Tree::from(&record.children[0])); widget.set_width_request(WIDTH);
*s.imp().record.borrow_mut() = Some(record); widget.set_height_request(HEIGHT);
s.set_width_request(WIDTH); // TODO: figure out the maximum width of the tree so that we can also set a width request
s.set_height_request(HEIGHT); 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(); let s = s.clone();
move |_, ctx, width, height| { move |_, ctx, width, height| {
s.redraw(ctx, width, height); s.redraw(ctx, width, height);
@ -66,27 +60,61 @@ impl ReviewTree {
s s
} }
pub fn redraw(&self, ctx: &Context, _width: i32, _height: i32) { fn redraw(&self, ctx: &Context, _width: i32, _height: i32) {
let tree: &Option<Tree<Uuid>> = &self.imp().tree.borrow(); println!("redraw: {} {}", _width, _height);
match tree {
Some(ref tree) => { #[allow(deprecated)]
for node in tree.bfs_iter() { let context = WidgetExt::style_context(&self.widget);
// draw a circle given the coordinates of the nodes #[allow(deprecated)]
// I don't know the indent. How do I keep track of that? Do I track the position of let foreground_color = context.lookup_color("sidebar_fg_color").unwrap();
// the parent? do I need to just make it more intrinsically a part of the position #[allow(deprecated)]
// code? let accent_color = context.lookup_color("accent_color").unwrap();
ctx.set_source_rgb(0.7, 0.7, 0.7);
self.view.map_tree(move |tree, node, current| {
let parent = tree.parent(node);
ctx.set_source_rgb(
foreground_color.red().into(),
foreground_color.green().into(),
foreground_color.blue().into(),
);
let (row, column) = tree.position(node.id); let (row, column) = tree.position(node.id);
let y = (row as f64) * 20. + 10.; let y = (row as f64) * SPACING + RADIUS * 2.;
let x = (column as f64) * 20. + 10.; let x = (column as f64) * SPACING + RADIUS * 2.;
ctx.arc(x, y, 5., 0., 2. * std::f64::consts::PI); 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) = tree.position(parent.id);
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.content) {
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.);
} }
None => { });
// if there is no tree present, then there's nothing to draw!
}
} }
pub fn widget(&self) -> gtk::Widget {
self.widget.clone().upcast::<gtk::Widget>()
} }
} }
@ -123,12 +151,6 @@ impl ReviewTree {
// A bottom-up traversal: // A bottom-up traversal:
// - Figure out the number of nodes at each depth // - Figure out the number of nodes at each depth
/*
struct Tree {
width: Vec<usize>, // the total width of the tree at each depth
}
*/
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;

View File

@ -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. // UI. So this will be a heavy lift on the UI side.
use crate::{ use crate::{
components::{Goban, PlayerCard, ReviewTree}, CoreApi, ResourceManager components::{Goban, PlayerCard, ReviewTree},
CoreApi, ResourceManager,
}; };
use glib::Object; use glib::Object;
use gtk::{prelude::*, subclass::prelude::*}; use gtk::{prelude::*, subclass::prelude::*};
use otg_core::Color; use otg_core::{Color, GameReviewViewModel};
use sgf::GameRecord; use sgf::GameRecord;
use std::{cell::RefCell, rc::Rc};
/*
#[derive(Default)] #[derive(Default)]
pub struct GameReviewPrivate {} pub struct GameReviewPrivate {
model: Rc<RefCell<Option<GameReviewViewModel>>>,
}
#[glib::object_subclass] #[glib::object_subclass]
impl ObjectSubclass for GameReviewPrivate { impl ObjectSubclass for GameReviewPrivate {
@ -52,13 +57,45 @@ impl GameReview {
pub fn new(_api: CoreApi, record: GameRecord, resources: ResourceManager) -> Self { pub fn new(_api: CoreApi, record: GameRecord, resources: ResourceManager) -> Self {
let s: Self = Object::builder().build(); 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. // 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 // 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. // renders the board and notifies the user of a problem that cannot be resolved.
/*
let board_repr = otg_core::Goban::default() let board_repr = otg_core::Goban::default()
.apply_moves(record.mainline()) .apply_moves(record.mainline())
.unwrap(); .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); 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 // 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 // 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. // 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 // 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 // keystrokes need to affect both the goban and the review tree simultanesouly. Possibly
@ -87,14 +124,18 @@ impl GameReview {
.spacing(4) .spacing(4)
.build(); .build();
player_information_section.append(&PlayerCard::new(Color::Black, &record.black_player)); player_information_section
player_information_section.append(&PlayerCard::new(Color::White, &record.white_player)); .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(&player_information_section);
sidebar.append(&review_tree); sidebar.append(&review_tree.widget());
s.append(&sidebar); self.widget.append(&sidebar);
}
s pub fn widget(&self) -> gtk::Widget {
self.widget.clone().upcast::<gtk::Widget>()
} }
} }