use serde::{ Serialize, Deserialize }; use fltk::*; use fltk::prelude::*; use fltk::enums::*; use fltk::image::PngImage; extern crate image; use image::io::Reader as ImageReader; use image::{ DynamicImage }; use std::collections::{HashMap, HashSet}; use std::cell::RefCell; use std::rc::Rc; use std::io::{ Write, Read, Cursor }; use std::fs::File; #[derive(Serialize, Deserialize, Debug)] struct Node { pub x: f32, pub y: f32, pub name: String, pub edges: HashSet, } #[derive(Serialize, Deserialize, Debug)] struct Board { nodes: HashMap, } impl Board { pub fn new() -> Self { let nodes = HashMap::new(); Board { nodes } } pub fn add_node(&mut self, x: f32, y: f32) { self.nodes.insert(self.next_id(), Node { x, y, name: "Canada".to_string(), edges: HashSet::new(), }); } pub fn remove_node(&mut self, id: usize) { self.nodes.remove(&id); for (_, node) in &mut self.nodes { node.edges.remove(&id); } } pub fn nearest_node(&self, x: f32, y: f32) -> Option { let f = |n: &Node| dist_sq((n.x, n.y), (x, y)); let mut iter = self.nodes.iter(); if let Some((id, node)) = iter.next() { let mut min = *id; let mut min_val = f(node); for (id, node) in iter { let val = f(node); if val < min_val { min = *id; min_val = val; } } Some(min) } else { None } } fn next_id(&self) -> usize { for i in 0 .. { if !self.nodes.contains_key(&i) { return i; } } unreachable!(); } } struct AppState { pub board: Board, pub image_raw: Option, pub image: Option, } impl AppState { fn new() -> Self { AppState { board: Board::new(), image_raw: None, image: None } } } fn menu_cb(_m: &mut impl MenuExt) { todo!(); } fn encode_png(image: &DynamicImage) -> Vec { let mut cursor = Cursor::new(Vec::new()); image.write_to(&mut cursor, image::ImageOutputFormat::Png).unwrap(); cursor.into_inner() } fn decode_png(buf: &[u8]) -> DynamicImage { let cursor = Cursor::new(buf); let mut reader = ImageReader::new(cursor); reader.set_format(image::ImageFormat::Png); reader.decode().unwrap() } fn lerp(v0: f32, v1: f32, t: f32) -> f32 { v0 + t * (v1 - v0) } fn inv_lerp(v0: f32, v1: f32, a: f32) -> f32 { (a - v0) / (v1 - v0) } fn dist_sq((x0, y0): (f32, f32), (x1, y1): (f32, f32)) -> f32 { (x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0) } trait CoordTransformer { fn to_coords(&self, x: i32, y: i32) -> (f32, f32); fn from_coords(&self, x: f32, y: f32) -> (i32, i32); } impl CoordTransformer for frame::Frame { fn to_coords(&self, x: i32, y: i32) -> (f32, f32) { ( inv_lerp(self.x() as f32, (self.x() + self.w()) as f32, x as f32), inv_lerp(self.y() as f32, (self.y() + self.h()) as f32, y as f32), ) } fn from_coords(&self, x: f32, y: f32) -> (i32, i32) { ( lerp(self.x() as f32, (self.x() + self.w()) as f32, x) as i32, lerp(self.y() as f32, (self.y() + self.h()) as f32, y) as i32, ) } } fn main() { let app = app::App::default() .with_scheme(app::Scheme::Gtk); let mut win = window::Window::default() .with_size(400, 300) .with_label("Board Builder"); let mut flex = group::Flex::default() .size_of_parent(); flex.set_type(group::FlexType::Column); // State let state = Rc::new(RefCell::new(AppState::new())); // Menu let mut menubar = menu::MenuBar::default(); let state_clone = Rc::clone(&state); menubar.add("File/New" , Shortcut::None, menu::MenuFlag::Normal, move |_| { state_clone.replace(AppState::new()); app::redraw(); }); let state_clone = Rc::clone(&state); menubar.add("File/Open..." , Shortcut::None, menu::MenuFlag::Normal, move |_| { let mut fc = dialog::NativeFileChooser::new(dialog::NativeFileChooserType::BrowseFile); fc.show(); let file = File::open(fc.filename()).unwrap(); let mut ar = zip::ZipArchive::new(file).unwrap(); let mut state = AppState::new(); let json_file = ar.by_name("graph.json").unwrap(); state.board = serde_json::from_reader(json_file).unwrap(); if let Ok(mut image_file) = ar.by_name("image.png") { let mut buf = Vec::new(); image_file.read_to_end(&mut buf).ok(); let image = decode_png(&buf); state.image = Some(PngImage::from_data(&buf).unwrap()); state.image_raw = Some(image); app::redraw(); } state_clone.replace(state); }); let state_clone = Rc::clone(&state); menubar.add("File/Open Image..." , Shortcut::None, menu::MenuFlag::Normal, move |_| { let mut fc = dialog::NativeFileChooser::new(dialog::NativeFileChooserType::BrowseFile); fc.show(); let filename = fc.filename(); match ImageReader::open(filename).map(|i| i.decode()) { Ok(Ok(image)) => { let mut state = state_clone.borrow_mut(); let data = encode_png(&image); state.image = Some(PngImage::from_data(&data).unwrap()); state.image_raw = Some(image); app::redraw(); } _ => dialog::alert_default("Error opening file"), } }); let state_clone = Rc::clone(&state); menubar.add("File/Save As ..." , Shortcut::None, menu::MenuFlag::Normal, move |_| { let mut fc = dialog::NativeFileChooser::new(dialog::NativeFileChooserType::BrowseSaveFile); fc.show(); let file = File::create(fc.filename()).unwrap(); let mut ar = zip::ZipWriter::new(file); let options = zip::write::FileOptions::default(); let state = state_clone.borrow(); ar.start_file("graph.json", options).ok(); ar.write(&serde_json::to_vec(&state.board).unwrap()).ok(); if let Some(image) = state.image_raw.as_ref() { ar.start_file("image.png", options).ok(); let data = encode_png(image); ar.write_all(&data).unwrap(); ar.flush().unwrap(); } ar.finish().ok(); }); menubar.add("Edit/Edit Nodes" , Shortcut::None, menu::MenuFlag::Normal, menu_cb); menubar.add("Edit/Edit Edges" , Shortcut::None, menu::MenuFlag::Normal, menu_cb); flex.set_size(&menubar, 40); // Canvas let mut frame = frame::Frame::default(); let state_clone = Rc::clone(&state); frame.draw(move |f| { use draw::*; let mut state = state_clone.borrow_mut(); let image = &mut state.image; if let Some(image) = image.as_mut() { image.scale(f.w(), f.h(), false, true); image.draw(f.x(), f.y(), f.w(), f.h()); } let board = &state.board; for (_, node) in &board.nodes { let (x, y) = f.from_coords(node.x, node.y); draw_text(&node.name, x, y); } }); let state_clone = Rc::clone(&state); frame.handle(move |f, e| { match e { Event::Push => { let mut state = state_clone.borrow_mut(); let (pos_x, pos_y) = f.to_coords(app::event_x(), app::event_y()); if app::event_button() == 1 { state.board.add_node(pos_x, pos_y); } else if app::event_button() == 3 { let id = state.board.nearest_node(pos_x, pos_y); if let Some(id) = id { state.board.remove_node(id); } } app::redraw(); true } _ => false, } }); flex.end(); win.end(); win.make_resizable(true); win.show(); app.run().unwrap(); }