diff --git a/otg/gtk/src/app_window.rs b/otg/gtk/src/app_window.rs index 6ff129b..b40c5e6 100644 --- a/otg/gtk/src/app_window.rs +++ b/otg/gtk/src/app_window.rs @@ -14,7 +14,7 @@ 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 . */ -use crate::CoreApi; +use crate::{CoreApi, ResourceManager}; use adw::prelude::*; use otg_core::{ @@ -22,7 +22,7 @@ use otg_core::{ CoreRequest, CoreResponse, }; use sgf::GameRecord; -use std::sync::{Arc, RwLock}; +use std::{rc::Rc, sync::{Arc, RwLock}}; use crate::views::{GameReview, HomeView, SettingsView}; @@ -58,10 +58,12 @@ pub struct AppWindow { // Not liking this, but I have to keep track of the settings view model separately from // anything else. I'll have to look into this later. settings_view_model: Arc>>, + + resources: ResourceManager, } impl AppWindow { - pub fn new(app: &adw::Application, core: CoreApi) -> Self { + pub fn new(app: &adw::Application, core: CoreApi, resources: ResourceManager) -> Self { let window = Self::setup_window(app); let overlay = Self::setup_overlay(); let stack = adw::NavigationView::new(); @@ -77,6 +79,7 @@ impl AppWindow { overlay, core, settings_view_model: Default::default(), + resources, }; let home = s.setup_home(); @@ -88,7 +91,7 @@ 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); + let game_review = GameReview::new(self.core.clone(), game_record, self.resources.clone()); let layout = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) diff --git a/otg/gtk/src/components/goban.rs b/otg/gtk/src/components/goban.rs index 7d9958b..85d52fe 100644 --- a/otg/gtk/src/components/goban.rs +++ b/otg/gtk/src/components/goban.rs @@ -35,7 +35,7 @@ You should have received a copy of the GNU General Public License along with On // Now, we know what kind of object we have for the current board representation. Let's make use of // that. -use crate::perftrace; +use crate::{perftrace, Resource, ResourceManager}; use gio::resources_lookup_data; use glib::Object; @@ -56,6 +56,7 @@ const MARGIN: i32 = 20; #[derive(Default)] pub struct GobanPrivate { board_state: Rc>, + resource_manager: Rc>>, } impl GobanPrivate {} @@ -88,10 +89,11 @@ glib::wrapper! { } impl Goban { - pub fn new(board_state: otg_core::Goban) -> Self { + 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); @@ -107,19 +109,42 @@ impl Goban { 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 { - Ok(Some(ref background)) => { + Some(Resource::Image(ref background)) => { ctx.set_source_pixbuf(background, 0., 0.); ctx.paint().expect("paint should never fail"); } - Ok(None) | Err(_) => ctx.set_source_rgb(0.7, 0.7, 0.7), + None => ctx.set_source_rgb(0.7, 0.7, 0.7), } let board = self.imp().board_state.borrow(); @@ -129,7 +154,14 @@ impl Goban { 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); + 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( @@ -179,35 +211,27 @@ struct Pen { y_offset: f64, hspace_between: f64, vspace_between: f64, - black_stone: Pixbuf, - white_stone: Pixbuf, + black_stone: Option, + white_stone: Option, } impl Pen { - fn new(x_offset: f64, y_offset: f64, hspace_between: f64, vspace_between: f64) -> Self { - let radius = (hspace_between / 2. - 2.) as i32; - let black_stone = load_pixbuf( - "/com/luminescent-dreams/otg-gtk/black_stone.png", - true, - 512, - 512, - ) - .unwrap() - .unwrap(); - let black_stone = black_stone - .scale_simple(radius * 2, radius * 2, InterpType::Nearest) - .unwrap(); - let white_stone = load_pixbuf( - "/com/luminescent-dreams/otg-gtk/white_stone.png", - true, - 512, - 512, - ) - .unwrap() - .unwrap(); - let white_stone = white_stone - .scale_simple(radius * 2, radius * 2, InterpType::Nearest) - .unwrap(); + 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, @@ -232,8 +256,14 @@ impl Pen { 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 => ctx.set_source_pixbuf(&self.white_stone, x_loc, y_loc), - Color::Black => ctx.set_source_pixbuf(&self.black_stone, x_loc, y_loc), + 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"); /* diff --git a/otg/gtk/src/lib.rs b/otg/gtk/src/lib.rs index e36a2bb..7fe1471 100644 --- a/otg/gtk/src/lib.rs +++ b/otg/gtk/src/lib.rs @@ -21,10 +21,12 @@ pub use app_window::AppWindow; mod views; -use async_std::task::{yield_now}; -use otg_core::{Core, Observable, CoreRequest, CoreResponse}; -use std::{rc::Rc}; - +use async_std::task::yield_now; +use gio::resources_lookup_data; +use gtk::gdk_pixbuf::{Colorspace, InterpType, Pixbuf}; +use image::{io::Reader as ImageReader, ImageError}; +use otg_core::{Core, CoreRequest, CoreResponse, Observable}; +use std::{cell::RefCell, collections::HashMap, io::Cursor, rc::Rc}; #[derive(Clone)] pub struct CoreApi { @@ -37,6 +39,93 @@ impl CoreApi { } } +#[derive(Clone)] +pub enum Resource { + Image(Pixbuf), +} + +#[derive(Clone)] +pub struct ResourceManager { + resources: Rc>>, +} + +impl ResourceManager { + pub fn new() -> Self { + let mut resources = HashMap::new(); + + for (path, xres, yres, transparency) in [ + ( + "/com/luminescent-dreams/otg-gtk/wood_texture.jpg", + 840, + 840, + false, + ), + ( + "/com/luminescent-dreams/otg-gtk/black_stone.png", + 40, + 40, + true, + ), + ( + "/com/luminescent-dreams/otg-gtk/white_stone.png", + 40, + 40, + true, + ), + ] { + match perftrace(&format!("loading {}", path), || { + Self::load_image(path, transparency, xres, yres) + }) { + Ok(Some(image)) => { + resources.insert(path.to_owned(), Resource::Image(image)); + } + Ok(None) => println!("no image in resource bundle for {}", path), + Err(err) => println!("failed to load image {}: {}", path, err), + } + } + + Self { + resources: Rc::new(RefCell::new(resources)), + } + } + + pub fn resource(&self, path: &str) -> Option { + self.resources.borrow().get(path).cloned() + } + + fn load_image( + 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) + }) + } + +} + pub fn perftrace(trace_name: &str, f: F) -> A where F: FnOnce() -> A, diff --git a/otg/gtk/src/main.rs b/otg/gtk/src/main.rs index 5d0519b..6035d16 100644 --- a/otg/gtk/src/main.rs +++ b/otg/gtk/src/main.rs @@ -4,8 +4,7 @@ use async_std::task::spawn; use gio::ActionEntry; use otg_core::{Config, ConfigOption, Core, CoreNotification, LibraryPath, Observable}; use otg_gtk::{ - AppWindow, - CoreApi, + AppWindow, CoreApi, ResourceManager }; @@ -123,8 +122,9 @@ fn main() { app.connect_activate({ move |app| { + let resources = ResourceManager::new(); let core_api = CoreApi { core: core.clone() }; - let app_window = AppWindow::new(app, core_api); + let app_window = AppWindow::new(app, core_api, resources); setup_app_configuration_action(app, app_window.clone()); diff --git a/otg/gtk/src/views/game_review.rs b/otg/gtk/src/views/game_review.rs index c68c19d..721c5d9 100644 --- a/otg/gtk/src/views/game_review.rs +++ b/otg/gtk/src/views/game_review.rs @@ -22,7 +22,9 @@ You should have received a copy of the GNU General Public License along with On // 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 crate::{components::{Goban, PlayerCard, ReviewTree}, CoreApi}; +use crate::{ + components::{Goban, PlayerCard, ReviewTree}, CoreApi, ResourceManager +}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use otg_core::Color; @@ -31,8 +33,6 @@ use sgf::GameRecord; #[derive(Default)] pub struct GameReviewPrivate {} - - #[glib::object_subclass] impl ObjectSubclass for GameReviewPrivate { const NAME: &'static str = "GameReview"; @@ -49,7 +49,7 @@ glib::wrapper! { } impl GameReview { - pub fn new(_api: CoreApi, record: GameRecord) -> Self { + pub fn new(_api: CoreApi, record: GameRecord, resources: ResourceManager) -> Self { let s: Self = Object::builder().build(); // It's actually really bad to be just throwing away errors. Panics make everyone unhappy. @@ -58,7 +58,7 @@ impl GameReview { let board_repr = otg_core::Goban::default() .apply_moves(record.mainline()) .unwrap(); - let board = Goban::new(board_repr); + let board = Goban::new(board_repr, resources); /* s.attach(&board, 0, 0, 2, 2);