Set up the game review page along with #229

Merged
savanni merged 24 commits from otg/game-review into main 2024-03-31 23:37:51 +00:00
7 changed files with 188 additions and 96 deletions
Showing only changes of commit 295f0a0411 - Show all commits

View File

@ -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<AppView>,
view_states: Vec<AppView>,
// 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(&gtk::Label::new(Some("Kifu")))
.title_widget(&gtk::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<AppView>) {
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<gtk::Widget>) -> adw::ViewStack {
// self.content.set_child(Some(content));
// }
}

View File

@ -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<RefCell<Color>>,
board: Rc<RefCell<BoardElement>>,
// board: Rc<RefCell<BoardElement>>,
cursor_location: Rc<RefCell<Option<Addr>>>,
api: Rc<RefCell<Option<CoreApi>>>,
@ -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;

View File

@ -45,41 +45,10 @@ impl Default for LibraryPrivate {
let model = gio::ListStore::new::<GameObject>();
model.extend_from_slice(&vector);
/*
let factory = gtk::SignalListItemFactory::new();
factory.connect_setup(move |_, list_item| {
let preview = GamePreview::new();
list_item
.downcast_ref::<gtk::ListItem>()
.expect("Needs to be a ListItem")
.set_child(Some(&preview));
});
factory.connect_bind(move |_, list_item| {
let game_element = list_item
.downcast_ref::<gtk::ListItem>()
.expect("Needs to be ListItem")
.item()
.and_downcast::<GameObject>()
.expect("The item has to be a GameObject.");
let preview = list_item
.downcast_ref::<gtk::ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<GamePreview>()
.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::<Vec<String>>()
.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::<GameObject>().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<Game>) {
let games = games
.into_iter()

View File

@ -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;

View File

@ -0,0 +1,64 @@
/*
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/>.
*/
// 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<GameReviewPrivate>) @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(&gtk::Label::new(Some("white player")), 0, 2, 1, 1);
s.attach(&gtk::Label::new(Some("black player")), 0, 2, 1, 2);
s.attach(&gtk::Label::new(Some("chat")), 1, 2, 2, 2);
s
}
}

View File

@ -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<HomePrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
pub struct HomeView(ObjectSubclass<HomePrivate>) @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)

View File

@ -1,3 +1,22 @@
/*
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::GameReview;
mod home;
pub use home::HomeView;