/* 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 . */ // I have an old Board class which I'm going to update. I'll just copy over the rendering code, but // at the same time I am going to work pretty heavily on the API. // // For a game review, the board needs to interact very well with a game record. So I have to keep // in mind all of that as I work on the API. // // Also, this is going to be a cross-platform application. Today it is Gnome + Rust, but as I // progress I will also need a Progressive Web App so that I can run this on my tablet. Especially // useful if I'm out socializing and happen to be talking to somebody who would enjoy a relaxing // game. Anyway, that is going to impact some of my API decisions. // // First, though, I need to rename my game record. // // Now, let's get the existing code compiling again. // // Okay, that wasn't so bad. I'm a little confused that I don't have a code action for renaming a // symbol, but I'll fix that some other time. Anyway, now let's focus on the goban. // Now, we know what kind of object we have for the current board representation. Let's make use of // that. 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 otg_core::{Color, Coordinate}; use std::{cell::RefCell, io::Cursor, rc::Rc}; const WIDTH: i32 = 800; const HEIGHT: i32 = 800; const MARGIN: i32 = 20; // Internal representation of the Goban drawing area. #[derive(Default)] pub struct GobanPrivate { board_state: Rc>, resource_manager: Rc>>, } impl GobanPrivate {} #[glib::object_subclass] impl ObjectSubclass for GobanPrivate { const NAME: &'static str = "Goban"; type Type = Goban; type ParentType = gtk::DrawingArea; } impl ObjectImpl for GobanPrivate {} impl WidgetImpl for GobanPrivate {} impl DrawingAreaImpl for GobanPrivate {} // This Goban, being in the `components` crate, is merely the rendering of a board. This is not // the primary representation of the board. // // In a game of Go, there are certain rules about what are valid moves and what are not. // Internally, I want to keep track of those, and doing so requires a few things. // // - We can never repeat a game state (though I think maybe that is allowed in a few rulesets, but // I'm coding to the AGA ruleset) // - We can never play a suicidal move // // Finally, updating the board state is non-GUI logic. So, sorry, might be dropping away from GUI // code for a while to work on the backend representation, some of which already exists. glib::wrapper! { pub struct Goban(ObjectSubclass) @extends gtk::DrawingArea, gtk::Widget; } impl Goban { pub fn new(board_state: otg_core::Goban, resources: ResourceManager) -> Self { let s: Self = Object::builder().build(); *s.imp().board_state.borrow_mut() = board_state; *s.imp().resource_manager.borrow_mut() = Some(resources); s.set_width_request(WIDTH); s.set_height_request(HEIGHT); s.set_draw_func({ let s = s.clone(); move |_, ctx, width, height| { perftrace("render drawing area", || s.redraw(ctx, width, height)); } }); s } 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 .borrow() .as_ref() .and_then(|r| r.resource("/com/luminescent-dreams/otg-gtk/wood_texture.jpg")); let black_texture = self .imp() .resource_manager .borrow() .as_ref() .and_then(|r| r.resource("/com/luminescent-dreams/otg-gtk/black_stone.png")); let white_texture = self .imp() .resource_manager .borrow() .as_ref() .and_then(|r| r.resource("/com/luminescent-dreams/otg-gtk/white_stone.png")); match background { Some(Resource::Image(ref background)) => { ctx.set_source_pixbuf(background, 0., 0.); ctx.paint().expect("paint should never fail"); } None => ctx.set_source_rgb(0.7, 0.7, 0.7), } let board = self.imp().board_state.borrow(); ctx.set_source_rgb(0.1, 0.1, 0.1); ctx.set_line_width(2.); let hspace_between = ((width - 40) as f64) / ((board.size.width - 1) as f64); let vspace_between = ((height - 40) as f64) / ((board.size.height - 1) as f64); let pen = Pen::new( MARGIN as f64, MARGIN as f64, hspace_between, vspace_between, black_texture, white_texture, ); (0..board.size.width).for_each(|col| { ctx.move_to( (MARGIN as f64) + (col as f64) * hspace_between, MARGIN as f64, ); ctx.line_to( (MARGIN as f64) + (col as f64) * hspace_between, (height as f64) - (MARGIN as f64), ); let _ = ctx.stroke(); }); (0..board.size.height).for_each(|row| { ctx.move_to( MARGIN as f64, (MARGIN as f64) + (row as f64) * vspace_between, ); ctx.line_to( (width - MARGIN) as f64, (MARGIN as f64) + (row as f64) * vspace_between, ); let _ = ctx.stroke(); }); // This doesn't work except on 19x19 boads. This code needs to be adjusted to at least // handle 13x13 and 9x9. Non-standard boards are On Their Own (TM). vec![3, 9, 15].into_iter().for_each(|col| { vec![3, 9, 15].into_iter().for_each(|row| { pen.star_point(ctx, col, row); }); }); (0..board.size.height).for_each(|row| { (0..board.size.width).for_each(|column| { match board.stone(&Coordinate { row, column }) { None => {} Some(Color::White) => pen.stone(ctx, row, column, Color::White, None), Some(Color::Black) => pen.stone(ctx, row, column, Color::Black, None), } }) }) } } struct Pen { x_offset: f64, y_offset: f64, hspace_between: f64, vspace_between: f64, black_stone: Option, white_stone: Option, } impl Pen { fn new( x_offset: f64, y_offset: f64, hspace_between: f64, vspace_between: f64, black_stone: Option, white_stone: Option, ) -> Self { let black_stone = match black_stone { Some(Resource::Image(img)) => Some(img), _ => None, }; let white_stone = match white_stone { Some(Resource::Image(img)) => Some(img), _ => None, }; Pen { x_offset, y_offset, hspace_between, vspace_between, black_stone, white_stone, } } fn star_point(&self, context: &cairo::Context, row: u8, col: u8) { context.arc( self.x_offset + (col as f64) * self.hspace_between, self.y_offset + (row as f64) * self.vspace_between, 5., 0., 2. * std::f64::consts::PI, ); let _ = context.fill(); } fn stone(&self, ctx: &cairo::Context, row: u8, col: u8, color: Color, _liberties: Option) { 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), 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), None => ctx.set_source_rgb(0.0, 0.0, 0.0), }, } ctx.paint().expect("paint should never fail"); /* match color { Color::White => ctx.set_source_rgb(0.9, 0.9, 0.9), Color::Black => ctx.set_source_rgb(0.0, 0.0, 0.0), }; self.draw_stone(ctx, row, col); */ /* if let Some(liberties) = liberties { let stone_location = self.stone_location(row, col); context.set_source_rgb(1., 0., 1.); context.set_font_size(32.); context.move_to(stone_location.0 - 10., stone_location.1 + 10.); let _ = context.show_text(&format!("{}", liberties)); } */ } #[allow(dead_code)] fn ghost_stone(&self, ctx: &cairo::Context, row: u8, col: u8, color: Color) { match color { Color::White => ctx.set_source_rgba(0.9, 0.9, 0.9, 0.5), Color::Black => ctx.set_source_rgba(0.0, 0.0, 0.0, 0.5), }; self.draw_stone(ctx, row, col); } fn draw_stone(&self, ctx: &cairo::Context, row: u8, col: u8) { let radius = self.hspace_between / 2. - 2.; let (x_loc, y_loc) = self.stone_location(row, col); ctx.arc(x_loc, y_loc, radius, 0.0, 2.0 * std::f64::consts::PI); let _ = ctx.fill(); } fn stone_location(&self, row: u8, col: u8) -> (f64, f64) { let radius = self.hspace_between / 2. - 2.; ( self.x_offset + (col as f64) * self.hspace_between - radius, self.y_offset + (row as f64) * self.vspace_between - radius, ) } } 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) }) }