From 95dc194d5da4afb5e3d1887a4caf2162274f6cec Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sat, 23 Mar 2024 11:21:10 -0400 Subject: [PATCH] Create the game review page and work on navigating to it with a navigation stack --- otg/gtk/src/app_window.rs | 98 ++++++++++++++++++------------- otg/gtk/src/components/board.rs | 23 ++++++-- otg/gtk/src/components/library.rs | 67 ++++++++------------- otg/gtk/src/components/mod.rs | 6 +- otg/gtk/src/views/game_review.rs | 64 ++++++++++++++++++++ otg/gtk/src/views/home.rs | 7 ++- otg/gtk/src/views/mod.rs | 19 ++++++ 7 files changed, 188 insertions(+), 96 deletions(-) create mode 100644 otg/gtk/src/views/game_review.rs diff --git a/otg/gtk/src/app_window.rs b/otg/gtk/src/app_window.rs index f7f4acf..ec7ae79 100644 --- a/otg/gtk/src/app_window.rs +++ b/otg/gtk/src/app_window.rs @@ -21,9 +21,10 @@ use otg_core::{ settings::{SettingsRequest, SettingsResponse}, Config, CoreRequest, CoreResponse, }; +use sgf::Game; use std::sync::{Arc, RwLock}; -use crate::views::{HomeView, SettingsView}; +use crate::views::{GameReview, HomeView, SettingsView}; #[derive(Clone)] enum AppView { @@ -37,7 +38,6 @@ enum AppView { #[derive(Clone)] pub struct AppWindow { pub window: adw::ApplicationWindow, - header: adw::HeaderBar, // content is a stack which contains the view models for the application. These are the main // elements that users want to interact with: the home page, the game library, a review, a game @@ -46,11 +46,11 @@ pub struct AppWindow { // we can maintain the state of previous views. Since the two of these work together, they are // a candidate for extraction into a new widget or a new struct. stack: adw::NavigationView, - content: Vec, + view_states: Vec, // Overlays are for transient content, such as about and settings, which can be accessed from // anywhere but shouldn't be part of the main application flow. - panel_overlay: gtk::Overlay, + overlay: gtk::Overlay, core: CoreApi, // Not liking this, but I have to keep track of the settings view model separately from @@ -61,28 +61,45 @@ pub struct AppWindow { impl AppWindow { pub fn new(app: &adw::Application, core: CoreApi) -> Self { let window = Self::setup_window(app); - let header = Self::setup_header(); - let panel_overlay = Self::setup_panel_overlay(); - let (stack, content) = Self::setup_content(core.clone()); + let overlay = Self::setup_overlay(); + let stack = adw::NavigationView::new(); + let view_states = vec![]; + + window.set_content(Some(&overlay)); + overlay.set_child(Some(&stack)); + + let s = Self { + window, + stack, + view_states, + overlay, + core, + settings_view_model: Default::default(), + }; + + let home = s.setup_home(); + + let _ = s.stack.push(&home); + + s + } + + pub fn open_game_review(&self, _game: Game) { + let header = adw::HeaderBar::new(); + let game_review = GameReview::new(self.core.clone()); let layout = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .build(); layout.append(&header); - layout.append(&panel_overlay); - panel_overlay.set_child(Some(&stack)); + layout.append(&game_review); - window.set_content(Some(&layout)); - - Self { - window, - header, - stack, - content, - panel_overlay, - core, - settings_view_model: Default::default(), - } + let page = adw::NavigationPage::builder() + .can_pop(true) + .title("Game Review") + .child(&layout) + .build(); + self.stack.push(&page); } pub fn open_settings(&self) { @@ -123,7 +140,7 @@ impl AppWindow { } }, ); - s.panel_overlay.add_overlay(&view_model); + s.overlay.add_overlay(&view_model); *s.settings_view_model.write().unwrap() = Some(view_model); } } @@ -134,7 +151,7 @@ impl AppWindow { let mut view = self.settings_view_model.write().unwrap(); match *view { Some(ref mut settings) => { - self.panel_overlay.remove_overlay(settings); + self.overlay.remove_overlay(settings); *view = None; } None => {} @@ -153,7 +170,7 @@ impl AppWindow { fn setup_header() -> adw::HeaderBar { let header = adw::HeaderBar::builder() - .title_widget(>k::Label::new(Some("Kifu"))) + .title_widget(>k::Label::new(Some("On the Grid"))) .build(); let app_menu = gio::Menu::new(); @@ -169,28 +186,27 @@ impl AppWindow { header } - fn setup_panel_overlay() -> gtk::Overlay { + fn setup_overlay() -> gtk::Overlay { gtk::Overlay::new() } - fn setup_content(core: CoreApi) -> (adw::NavigationView, Vec) { - let stack = adw::NavigationView::new(); - let content = Vec::new(); + fn setup_home(&self) -> adw::NavigationPage { + let header = Self::setup_header(); + let home = HomeView::new(self.core.clone(), { + let s = self.clone(); + move |game| s.open_game_review(game) + }); - let home = HomeView::new(core.clone()); - let _ = stack.push( - &adw::NavigationPage::builder() - .can_pop(false) - .title("Kifu") - .child(&home) - .build(), - ); - // content.push(AppView::Home(HomeViewModel::new(core))); + let layout = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .build(); + layout.append(&header); + layout.append(&home); - (stack, content) + adw::NavigationPage::builder() + .can_pop(false) + .title("Home") + .child(&layout) + .build() } - - // pub fn set_content(content: &impl IsA) -> adw::ViewStack { - // self.content.set_child(Some(content)); - // } } diff --git a/otg/gtk/src/components/board.rs b/otg/gtk/src/components/board.rs index eba5fbf..4d0a6fb 100644 --- a/otg/gtk/src/components/board.rs +++ b/otg/gtk/src/components/board.rs @@ -8,7 +8,6 @@ use gtk::{ }; use image::io::Reader as ImageReader; use otg_core::{ - ui::{BoardElement, IntersectionElement}, Color, }; use std::{cell::RefCell, io::Cursor, rc::Rc}; @@ -27,7 +26,7 @@ pub struct BoardPrivate { drawing_area: gtk::DrawingArea, current_player: Rc>, - board: Rc>, + // board: Rc>, cursor_location: Rc>>, api: Rc>>, @@ -43,7 +42,7 @@ impl ObjectSubclass for BoardPrivate { BoardPrivate { drawing_area: Default::default(), current_player: Rc::new(RefCell::new(Color::Black)), - board: Default::default(), + // board: Default::default(), cursor_location: Default::default(), api: Default::default(), } @@ -55,7 +54,7 @@ impl ObjectImpl for BoardPrivate { self.drawing_area.set_width_request(WIDTH); self.drawing_area.set_height_request(HEIGHT); - let board = self.board.clone(); + // let board = self.board.clone(); let cursor_location = self.cursor_location.clone(); let current_player = self.current_player.clone(); @@ -85,6 +84,7 @@ impl ObjectImpl for BoardPrivate { .set_draw_func(move |_, context, width, height| { perftrace("render drawing area", || { let render_start = std::time::Instant::now(); + /* let board = board.borrow(); match background { @@ -137,7 +137,9 @@ impl ObjectImpl for BoardPrivate { pen.star_point(context, col, row); }); }); + */ + /* (0..19).for_each(|col| { (0..19).for_each(|row| { if let IntersectionElement::Filled(stone) = board.stone(row, col) { @@ -162,6 +164,7 @@ impl ObjectImpl for BoardPrivate { } } } + */ let render_end = std::time::Instant::now(); println!("board rendering time: {:?}", render_end - render_start); }) @@ -169,10 +172,11 @@ impl ObjectImpl for BoardPrivate { let motion_controller = gtk::EventControllerMotion::new(); { - let board = self.board.clone(); + // let board = self.board.clone(); let cursor = self.cursor_location.clone(); let drawing_area = self.drawing_area.clone(); motion_controller.connect_motion(move |_, x, y| { + /* let board = board.borrow(); let mut cursor = cursor.borrow_mut(); let hspace_between = ((WIDTH - 40) as f64) / ((board.size.width - 1) as f64); @@ -195,17 +199,21 @@ impl ObjectImpl for BoardPrivate { *cursor = addr; drawing_area.queue_draw(); } + */ }); } let gesture = gtk::GestureClick::new(); { - let board = self.board.clone(); + // let board = self.board.clone(); let cursor = self.cursor_location.clone(); let api = self.api.clone(); gesture.connect_released(move |_, _, _, _| { + /* let board = board.borrow(); let cursor = cursor.borrow(); + */ + /* match *cursor { None => {} Some(ref cursor) => { @@ -220,6 +228,7 @@ impl ObjectImpl for BoardPrivate { } } } + */ }); } @@ -244,10 +253,12 @@ impl Board { s } + /* pub fn set_board(&self, board: BoardElement) { *self.imp().board.borrow_mut() = board; self.imp().drawing_area.queue_draw(); } + */ pub fn set_current_player(&self, color: Color) { *self.imp().current_player.borrow_mut() = color; diff --git a/otg/gtk/src/components/library.rs b/otg/gtk/src/components/library.rs index f0d40a0..81510bb 100644 --- a/otg/gtk/src/components/library.rs +++ b/otg/gtk/src/components/library.rs @@ -45,41 +45,10 @@ impl Default for LibraryPrivate { let model = gio::ListStore::new::(); model.extend_from_slice(&vector); - /* - let factory = gtk::SignalListItemFactory::new(); - - factory.connect_setup(move |_, list_item| { - let preview = GamePreview::new(); - list_item - .downcast_ref::() - .expect("Needs to be a ListItem") - .set_child(Some(&preview)); - }); - factory.connect_bind(move |_, list_item| { - let game_element = list_item - .downcast_ref::() - .expect("Needs to be ListItem") - .item() - .and_downcast::() - .expect("The item has to be a GameObject."); - - let preview = list_item - .downcast_ref::() - .expect("Needs to be ListItem") - .child() - .and_downcast::() - .expect("The child has to be a GamePreview object."); - - match game_element.game() { - Some(game) => preview.set_game(game), - None => (), - }; - }); - */ - let selection_model = gtk::NoSelection::new(Some(model.clone())); let list_view = gtk::ColumnView::builder() .model(&selection_model) + .single_click_activate(true) .hexpand(true) .build(); @@ -116,17 +85,7 @@ impl Default for LibraryPrivate { .factory(&make_factory(|g| { g.dates .iter() - .map(|date| { - format!("{}", date) - /* - let l = locale!("en-US").into(); - let options = length::Bag::from_date_style(length::Date::Medium); - let date = Date::try_new_iso_date(date. - let dtfmt = - DateFormatter::try_new_with_length(&l, options).unwrap(); - dtfmt.format(date).unwrap() - */ - }) + .map(|date| format!("{}", date)) .collect::>() .join(", ") })) @@ -196,6 +155,28 @@ impl Default for Library { } impl Library { + pub fn new(on_select: impl Fn(Game) + 'static) -> Library { + let s = Library::default(); + + s.imp().list_view.connect_activate({ + let s = s.clone(); + move |_, row_id| { + println!("row activated: {}", row_id); + + let object = s.imp().model.item(row_id); + let game = object.and_downcast_ref::().unwrap(); + println!( + "{:?} vs. {:?}", + game.game().unwrap().white_player, + game.game().unwrap().black_player + ); + on_select(game.game().unwrap()); + } + }); + + s + } + pub fn set_games(&self, games: Vec) { let games = games .into_iter() diff --git a/otg/gtk/src/components/mod.rs b/otg/gtk/src/components/mod.rs index d673a45..d8fc0be 100644 --- a/otg/gtk/src/components/mod.rs +++ b/otg/gtk/src/components/mod.rs @@ -1,3 +1,6 @@ +mod board; +pub use board::Board; + // mod chat; // pub use chat::Chat; @@ -19,9 +22,6 @@ pub use library::Library; // mod home; // pub use home::Home; -// mod board; -// pub use board::Board; - #[cfg(feature = "screenplay")] pub use playing_field::playing_field_view; diff --git a/otg/gtk/src/views/game_review.rs b/otg/gtk/src/views/game_review.rs new file mode 100644 index 0000000..d29e534 --- /dev/null +++ b/otg/gtk/src/views/game_review.rs @@ -0,0 +1,64 @@ +/* +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 . +*/ + +// Game review consists of the board, some information about the players, the game tree, and any +// commentary on the current move. This requires four major components, some of which are easier +// than others. The game board has to be kept in sync with the game tree, so there's a +// communication channel there. +// +// 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 glib::Object; +use gtk::{prelude::*, subclass::prelude::*}; +use crate::{components::Board, CoreApi}; + +pub struct GameReviewPrivate {} + +impl Default for GameReviewPrivate { + fn default() -> Self { + Self {} + } +} + +#[glib::object_subclass] +impl ObjectSubclass for GameReviewPrivate { + const NAME: &'static str = "GameReview"; + type Type = GameReview; + type ParentType = gtk::Grid; +} + +impl ObjectImpl for GameReviewPrivate {} +impl WidgetImpl for GameReviewPrivate {} +impl GridImpl for GameReviewPrivate {} + +glib::wrapper! { + pub struct GameReview(ObjectSubclass) @extends gtk::Grid, gtk::Widget, @implements gtk::Accessible; +} + +impl GameReview { + pub fn new(api: CoreApi) -> Self { + let s: Self = Object::builder().build(); + + let board = Board::new(api); + s.attach(&board, 0, 0, 2, 2); + s.attach(>k::Label::new(Some("white player")), 0, 2, 1, 1); + s.attach(>k::Label::new(Some("black player")), 0, 2, 1, 2); + s.attach(>k::Label::new(Some("chat")), 1, 2, 2, 2); + + s + } +} diff --git a/otg/gtk/src/views/home.rs b/otg/gtk/src/views/home.rs index 02bb541..40e5dfc 100644 --- a/otg/gtk/src/views/home.rs +++ b/otg/gtk/src/views/home.rs @@ -21,6 +21,7 @@ use otg_core::{ library::{LibraryRequest, LibraryResponse}, CoreRequest, CoreResponse, }; +use sgf::Game; use std::{cell::RefCell, rc::Rc}; /* @@ -126,11 +127,11 @@ impl WidgetImpl for HomePrivate {} impl BoxImpl for HomePrivate {} glib::wrapper! { - pub struct HomeView(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; + pub struct HomeView(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable, gtk::Accessible; } impl HomeView { - pub fn new(api: CoreApi) -> Self { + pub fn new(api: CoreApi, on_select_game: impl Fn(Game) + 'static) -> Self { let s: Self = Object::builder().build(); s.set_spacing(4); s.set_homogeneous(false); @@ -157,7 +158,7 @@ impl HomeView { s.append(&new_game_button); */ - let library = Library::default(); + let library = Library::new(move |game| on_select_game(game)); let library_view = gtk::ScrolledWindow::builder() .hscrollbar_policy(gtk::PolicyType::Never) .min_content_width(360) diff --git a/otg/gtk/src/views/mod.rs b/otg/gtk/src/views/mod.rs index 414675c..2e40804 100644 --- a/otg/gtk/src/views/mod.rs +++ b/otg/gtk/src/views/mod.rs @@ -1,3 +1,22 @@ +/* +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::GameReview; + mod home; pub use home::HomeView;