/* 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 . */ pub mod components; mod app_window; pub use app_window::AppWindow; mod views; 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 { pub core: Core, } impl CoreApi { pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse { self.core.dispatch(request).await } } #[derive(Clone)] pub enum Resource { Image(Pixbuf), } #[derive(Clone)] pub struct ResourceManager { resources: Rc>>, } impl Default for ResourceManager { fn default() -> 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)), } } } impl ResourceManager { 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, { let start = std::time::Instant::now(); let result = f(); let end = std::time::Instant::now(); println!("[Trace: {}] {:?}", trace_name, end - start); result } /// LocalObserver creates a task on the current thread which watches the specified observer for notifications and calls the handler function with each one. /// /// The LocalObserver starts a task which listens for notifications during the constructor. When the observer goes out of scope, it will make a point of aborting the task. This combination means that anything which uses the observer can create it, hold on to a reference of it, and then drop it when done, and not have to do anything else with the observer object. #[allow(dead_code)] struct LocalObserver { join_handle: glib::JoinHandle<()>, handler: Rc, } impl LocalObserver { /// Construct a new LocalObserver and start it running. /// /// observable -- any object which emits events /// handler -- a function which can process events #[allow(dead_code)] fn new(observable: &dyn Observable, handler: impl Fn(T) + 'static) -> Self { let listener = observable.subscribe(); let handler = Rc::new(handler); let join_handle = glib::spawn_future_local({ let handler = handler.clone(); async move { loop { match listener.recv().await { Ok(msg) => handler(msg), Err(_) => { // recv only fails if the channel has been closed and no other notifications are pending. This will break out of the loop and terminate the observer. return; } } yield_now().await; } } }); Self { join_handle, handler, } } } impl Drop for LocalObserver { fn drop(&mut self) { // Abort the task when the observer goes out of scope. self.join_handle.abort(); } }