187 lines
5.8 KiB
Rust
187 lines
5.8 KiB
Rust
/*
|
|
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/>.
|
|
*/
|
|
|
|
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<RefCell<HashMap<String, Resource>>>,
|
|
}
|
|
|
|
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<Resource> {
|
|
self.resources.borrow().get(path).cloned()
|
|
}
|
|
|
|
fn load_image(
|
|
path: &str,
|
|
transparency: bool,
|
|
width: i32,
|
|
height: i32,
|
|
) -> Result<Option<Pixbuf>, 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<F, A>(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<T> {
|
|
join_handle: glib::JoinHandle<()>,
|
|
handler: Rc<dyn Fn(T)>,
|
|
}
|
|
|
|
impl<T: 'static> LocalObserver<T> {
|
|
/// 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<T>, 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<T> Drop for LocalObserver<T> {
|
|
fn drop(&mut self) {
|
|
// Abort the task when the observer goes out of scope.
|
|
self.join_handle.abort();
|
|
}
|
|
}
|