/* Copyright 2023, Savanni D'Gerinel This file is part of the Luminescent Dreams Tools. Luminescent Dreams Tools 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. Luminescent Dreams Tools 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 Lumeto. If not, see . */ use cairo::Context; use coordinates::{hex_map::parse_data, AxialAddr}; use gio::resources_lookup_data; use glib::{subclass::InitializingObject, Object}; use gtk::{gio, prelude::*, subclass::prelude::*, Application, DrawingArea}; use image::io::Reader as ImageReader; use std::{cell::RefCell, io::Cursor, rc::Rc}; mod labeled_field; mod palette_entry; mod tile; mod utilities; const APP_ID: &'static str = "com.luminescent-dreams.hex-grid"; const HEX_RADIUS: f64 = 50.; const MAP_RADIUS: usize = 3; const DRAWING_ORIGIN: (f64, f64) = (1024. / 2., 768. / 2.); fn main() { gio::resources_register_include!("com.luminescent-dreams.hex-grid.gresource") .expect("Failed to register resources"); let app = Application::builder().application_id(APP_ID).build(); app.connect_activate(|app| { let window = HexGridWindow::new(app); window.present(); }); app.run(); } pub struct HexGridWindowPrivate { layout: gtk::Box, palette: gtk::Box, drawing_area: DrawingArea, hex_address: labeled_field::LabeledField, canvas_address: labeled_field::LabeledField, current_coordinate: Rc>>, } #[glib::object_subclass] impl ObjectSubclass for HexGridWindowPrivate { const NAME: &'static str = "HexGridWindow"; type Type = HexGridWindow; type ParentType = gtk::ApplicationWindow; fn new() -> Self { println!("hexGridWindowPrivate::new()"); let current_coordinate = Rc::new(RefCell::new(None)); let drawing_area = DrawingArea::builder() .width_request(1024) .height_request(768) .margin_start(8) .margin_end(8) .margin_top(8) .margin_bottom(8) .hexpand(true) .build(); let layout = gtk::Box::builder() .homogeneous(false) .spacing(8) .can_focus(false) .visible(true) .build(); let sidebar = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .width_request(100) .visible(true) .can_focus(false) .margin_start(4) .margin_end(4) .margin_top(4) .margin_bottom(4) .spacing(8) .build(); let canvas_address = labeled_field::LabeledField::new("Canvas Address", "-----"); let hex_address = labeled_field::LabeledField::new("Hex Address", "-----"); let palette = gtk::Box::builder() .spacing(8) .orientation(gtk::Orientation::Vertical) .hexpand(true) .build(); sidebar.append(&canvas_address); sidebar.append(&hex_address); sidebar.append(&palette); layout.append(&drawing_area); layout.append(&sidebar); layout.show(); Self { drawing_area, hex_address, canvas_address, current_coordinate, layout, palette, } } } impl ObjectImpl for HexGridWindowPrivate { fn constructed(&self) { println!("HexGridWindowPrivate::constructed()"); self.parent_constructed(); let map_text_resource = resources_lookup_data( "/com/luminescent-dreams/hex-grid/map.txt", gio::ResourceLookupFlags::NONE, ) .expect("map should be in the bundle") .to_vec(); let hex_map = parse_data::(String::from_utf8(map_text_resource).unwrap().lines()); let terrain_data = resources_lookup_data( "/com/luminescent-dreams/hex-grid/terrain.ppm", gio::ResourceLookupFlags::NONE, ) .unwrap(); let reader = ImageReader::new(Cursor::new(terrain_data)) .with_guessed_format() .unwrap(); let image = reader.decode().unwrap(); let badlands = tile::Tile::new(&image, tile::Terrain::Badlands); let deep_water = tile::Tile::new(&image, tile::Terrain::DeepWater); let desert = tile::Tile::new(&image, tile::Terrain::Desert); let grasslands = tile::Tile::new(&image, tile::Terrain::Grasslands); let mountain = tile::Tile::new(&image, tile::Terrain::Mountain); let shallow_water = tile::Tile::new(&image, tile::Terrain::ShallowWater); let swamp = tile::Tile::new(&image, tile::Terrain::Swamp); self.palette .append(&palette_entry::PaletteEntry::new(&badlands)); self.palette .append(&palette_entry::PaletteEntry::new(&deep_water)); self.palette .append(&palette_entry::PaletteEntry::new(&desert)); self.palette .append(&palette_entry::PaletteEntry::new(&grasslands)); self.palette .append(&palette_entry::PaletteEntry::new(&mountain)); self.palette .append(&palette_entry::PaletteEntry::new(&shallow_water)); self.palette .append(&palette_entry::PaletteEntry::new(&swamp)); let motion_controller = gtk::EventControllerMotion::new(); { let canvas_address = self.canvas_address.clone(); let hex_address = self.hex_address.clone(); let c = self.current_coordinate.clone(); motion_controller.connect_motion(move |_, x, y| { let norm_x = x - DRAWING_ORIGIN.0; let norm_y = y - DRAWING_ORIGIN.1; let q = (2. / 3. * norm_x) / HEX_RADIUS; let r = (-1. / 3. * norm_x + (3. as f64).sqrt() / 3. * norm_y) / HEX_RADIUS; let (q, r) = axial_round(q, r); let coordinate = AxialAddr::new(q, r); canvas_address.set_value(&format!("{:.0} {:.0}", x, y)); if coordinate.distance(&AxialAddr::origin()) > MAP_RADIUS { hex_address.set_value(&format!("-----")); *c.borrow_mut() = None; } else { hex_address.set_value(&format!("{:.0} {:.0}", coordinate.q(), coordinate.r())); *c.borrow_mut() = Some(coordinate); } }); } { self.drawing_area.set_draw_func(move |_, context, _, _| { context.set_source_rgb(0., 0., 0.); let _ = context.paint(); context.set_line_width(2.); for coordinate in vec![AxialAddr::origin()] .into_iter() .chain(AxialAddr::origin().addresses(MAP_RADIUS)) { let center_x = DRAWING_ORIGIN.0 + HEX_RADIUS * (3. / 2. * (coordinate.q() as f64)); let center_y = DRAWING_ORIGIN.1 + HEX_RADIUS * ((3. as f64).sqrt() / 2. * (coordinate.q() as f64) + (3. as f64).sqrt() * (coordinate.r() as f64)); let translate_x = center_x - HEX_RADIUS; let translate_y = center_y - (3. as f64).sqrt() * HEX_RADIUS / 2.; let tile = match hex_map.get(&coordinate).unwrap() { tile::Terrain::Mountain => &mountain, tile::Terrain::Grasslands => &grasslands, tile::Terrain::ShallowWater => &shallow_water, tile::Terrain::DeepWater => &deep_water, tile::Terrain::Badlands => &badlands, tile::Terrain::Desert => &desert, tile::Terrain::Swamp => &swamp, _ => panic!("unhandled terrain type"), }; tile.render_on_context(context, translate_x, translate_y); } }); } self.drawing_area.add_controller(motion_controller); } } impl WidgetImpl for HexGridWindowPrivate {} impl WindowImpl for HexGridWindowPrivate {} impl ApplicationWindowImpl for HexGridWindowPrivate {} glib::wrapper! { pub struct HexGridWindow(ObjectSubclass) @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget, @implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget; } impl HexGridWindow { pub fn new(app: &Application) -> Self { let window: Self = Object::builder().property("application", app).build(); window.set_child(Some(&window.imp().layout)); window } } fn draw_hexagon(context: &Context, center_x: f64, center_y: f64, radius: f64) { let ul_x = center_x - radius; let ul_y = center_y - (3. as f64).sqrt() * radius / 2.; let points: Vec<(f64, f64)> = utilities::hexagon(radius * 2., (3. as f64).sqrt() * radius); context.new_path(); context.move_to(ul_x + points[0].0, ul_y + points[0].1); context.line_to(ul_x + points[1].0, ul_y + points[1].1); context.line_to(ul_x + points[2].0, ul_y + points[2].1); context.line_to(ul_x + points[3].0, ul_y + points[3].1); context.line_to(ul_x + points[4].0, ul_y + points[4].1); context.line_to(ul_x + points[5].0, ul_y + points[5].1); context.close_path(); } fn axial_round(q_f64: f64, r_f64: f64) -> (i32, i32) { let s_f64 = -q_f64 - r_f64; let mut q = q_f64.round(); let mut r = r_f64.round(); let s = s_f64.round(); let q_diff = (q - q_f64).abs(); let r_diff = (r - r_f64).abs(); let s_diff = (s - s_f64).abs(); if q_diff > r_diff && q_diff > s_diff { q = -r - s; } else if r_diff > s_diff { r = -q - s; } unsafe { (q.to_int_unchecked::(), r.to_int_unchecked::()) } }