#![windows_subsystem = "windows"] // Don't show a console use eframe::egui; use egui::*; use image::DynamicImage; use rfd::FileDialog; use board_builder::{ Board, Node, CoordTransformer }; use board_builder::io::{ read_board_from_file, write_board_to_file }; use std::path::Path; use std::collections::HashSet; use std::rc::Rc; use std::cell::RefCell; fn main() { let native_options = eframe::NativeOptions::default(); eframe::run_native("Board Builder", native_options, Box::new(|cc| Box::new(BoardBuilderApp::new(cc)))); } #[derive(Default)] struct BoardBuilderApp { board: Rc>, texture: Option, image: Option, edit_mode: EditMode, selected_node: Option, create_node_dialog: CreateNodeDialog, edit_node_dialog: EditNodeDialog, edit_labels_dialog: EditLabelsDialog, edit_settings_dialog: EditSettingsDialog, } #[derive(Clone, Copy)] enum EditMode { Nodes, Edges, } impl Default for EditMode { fn default() -> Self { EditMode::Nodes } } impl eframe::App for BoardBuilderApp { fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) { TopBottomPanel::top("menubar").show(ctx, |ui| { menu::bar(ui, |ui| { ui.menu_button("File", |ui| { if ui.button("New").clicked() { self.board.replace(Board::default()); self.texture = None; self.image = None; self.edit_mode = EditMode::Nodes; ui.close_menu(); } if ui.button("Open...").clicked() { if let Some(board_file) = FileDialog::new().pick_file() { match read_board_from_file(&board_file) { Ok((board, image)) => { self.board.replace(board); match image { None => { self.image = None; self.texture = None } Some(image) => self.load_image(ctx, image), } } Err(_) => panic!("Could not open file!"), } } ui.close_menu(); } if ui.button("Save As...").clicked() { if let Some(board_file) = FileDialog::new().save_file() { write_board_to_file(&self.board.borrow(), self.image.as_ref(), &board_file).expect("Something went wrong saving!"); } } if ui.button("Open Image...").clicked() { let image_file = FileDialog::new() .add_filter("Image", &["png", "jpg", "jpeg", "gif", "webp", "bmp", "tiff"]) .pick_file(); if let Some(image_file) = image_file { self.load_image_file(ctx, &image_file).unwrap(); } } }); ui.menu_button("Edit", |ui| { if ui.button("Edit Nodes").clicked() { self.edit_mode = EditMode::Nodes; } if ui.button("Edit Edges").clicked() { self.edit_mode = EditMode::Edges; } if ui.button("Edit Labels...").clicked() { self.edit_labels_dialog.show(Rc::clone(&self.board)); } }); if ui.button("Settings...").clicked() { self.edit_settings_dialog.show(Rc::clone(&self.board)); }; }); }); CentralPanel::default().show(ctx, |ui| { if let Some(texture) = self.texture.as_ref() { let size = ui.available_size(); let (response, painter) = ui.allocate_painter(size, Sense::click()); let image = widgets::Image::new(texture, size); image.paint_at(ui, response.rect); let view = View(response.rect); self.draw_board(&painter, view); if let Some(pos) = response.interact_pointer_pos() { let btn = if response.clicked() { PointerButton::Primary } else if response.secondary_clicked() { PointerButton::Secondary } else { PointerButton::Middle }; let (x, y) = view.xform(pos); self.dispatch_click(btn, x, y); } } }); self.create_node_dialog.ui(ctx); self.edit_node_dialog.ui(ctx); self.edit_labels_dialog.ui(ctx); self.edit_settings_dialog.ui(ctx); } } impl BoardBuilderApp { fn new(cc: &eframe::CreationContext<'_>) -> Self { let mut style = (*cc.egui_ctx.style()).clone(); let mut button = style::TextStyle::Button.resolve(&style); button.size = 20.0; style.text_styles.insert(style::TextStyle::Button, button); cc.egui_ctx.set_style(style); BoardBuilderApp::default() } fn load_image_file(&mut self, ctx: &Context, image_file: &Path) -> Result<(), image::ImageError> { let image = image::io::Reader::open(image_file)?.decode()?; self.load_image(ctx, image); Ok(()) } fn load_image(&mut self, ctx: &Context, image: DynamicImage) { let egui_image = egui::ColorImage::from_rgba_unmultiplied( [image.width() as _, image.height() as _], image.to_rgba8().as_flat_samples().as_slice(), ); self.image = Some(image); self.texture = Some(ctx.load_texture("board-image", egui_image)); } fn draw_board(&self, painter: &Painter, view: View) { let board = self.board.borrow(); for (&id, node) in &board.nodes { let color = if Some(id) == self.selected_node { Color32::RED } else { Color32::BLACK }; painter.text( view.inv_xform(node.x, node.y), Align2::CENTER_CENTER, &node.name, FontId::proportional(16.0), color, ); let stroke = Stroke { width: 1.0, color: Color32::BLACK }; for edge in &node.edges { let other_node = &board.nodes[edge]; if board.config.horizontal_wrapping && node.should_wrap_horizontal(other_node) { let mut nodes = [node, other_node]; nodes.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap()); let [left_node, right_node] = nodes; let y_mid = left_node.y_mid(right_node); painter.line_segment( [view.inv_xform(0.0, y_mid), view.inv_xform(left_node.x, left_node.y)], stroke ); painter.line_segment( [view.inv_xform(right_node.x, right_node.y), view.inv_xform(1.0, y_mid)], stroke ); } else { painter.line_segment( [view.inv_xform(node.x, node.y), view.inv_xform(other_node.x, other_node.y)], stroke, ); } } } } fn dispatch_click(&mut self, btn: PointerButton, x: f32, y: f32) { use EditMode::*; use PointerButton::*; match (btn, self.edit_mode) { (Primary, Nodes) => { let board = Rc::clone(&self.board); self.create_node_dialog.show(x, y, board); } (Primary, Edges) => self.select_edge(x, y), (Secondary, Nodes) => { let board = Rc::clone(&self.board); if let Some(id) = self.board.borrow().nearest_node(x, y) { self.edit_node_dialog.show(id, board); } }, _ => {}, } } fn select_edge(&mut self, x: f32, y: f32) { let mut board = self.board.borrow_mut(); if let Some(nearest_id) = board.nearest_node(x, y) { match self.selected_node { None => self.selected_node = Some(nearest_id), Some(id) if id == nearest_id => self.selected_node = None, Some(id) => board.add_edge(id, nearest_id), } } } } #[derive(Clone, Copy)] struct View(Rect); impl CoordTransformer for View { fn origin(&self) -> Pos2 { self.0.min } fn extremes(&self) -> Pos2 { self.0.max } } #[derive(Default)] struct CreateNodeDialog { open: bool, node: Node, board: Rc>, } impl CreateNodeDialog { fn show(&mut self, x: f32, y: f32, board: Rc>) { self.open = true; self.board = board; self.node = Node { x, y, ..Node::default() } } fn ui(&mut self, ctx: &Context) { if !self.open { return } Window::new("Create Node") .collapsible(false) .show(ctx, |ui| { let mut board = self.board.borrow_mut(); node_common_ui(ui, &mut self.node, &board); if ui.button("Ok").clicked() { board.insert_node(self.node.clone()); self.open = false; } if ui.button("Cancel").clicked() { self.open = false; } }); } } #[derive(Default)] struct EditNodeDialog { open: bool, id: usize, node: Node, board: Rc>, } impl EditNodeDialog { fn show(&mut self, id: usize, board: Rc>) { self.open = true; self.id = id; self.board = board; self.node = self.board.borrow().nodes.get(&self.id).unwrap().clone(); } fn ui(&mut self, ctx: &Context) { if !self.open { return } Window::new("Edit Node") .collapsible(false) .show(ctx, |ui| { let mut board = self.board.borrow_mut(); node_common_ui(ui, &mut self.node, &board); if ui.button("Ok").clicked() { board.nodes.insert(self.id, self.node.clone()); self.open = false; } if ui.button("Delete").clicked() { board.remove_node(self.id); self.open = false; } if ui.button("Cancel").clicked() { self.open = false; } }); } } #[derive(Default)] struct EditLabelsDialog { open: bool, board: Rc>, selected_label_key: String, selected_label_value: String, add_key_dialog: StringDialog, add_value_dialog: StringDialog, } impl EditLabelsDialog { fn show(&mut self, board: Rc>) { self.board = board; self.open = true; } fn ui(&mut self, ctx: &Context) { Window::new("Edit Labels") .collapsible(false) .open(&mut self.open) .show(ctx, |ui| { let mut board = self.board.borrow_mut(); let selected_label_key = &mut self.selected_label_key; let selected_label_value = &mut self.selected_label_value; ui.columns(2, |col| { col[0].group(|ui| { for key_label in board.labels.keys() { ui.selectable_value(selected_label_key, key_label.clone(), key_label); } }); col[0].horizontal(|ui| { if ui.button("+").clicked() { self.add_key_dialog.show(); } if ui.button("-").clicked() { board.remove_label_key(selected_label_key); *selected_label_key = String::new(); *selected_label_value = String::new(); } }); col[1].group(|ui| { if let Some(value_labels) = board.labels.get(selected_label_key) { for value_label in value_labels { ui.selectable_value(selected_label_value, value_label.clone(), value_label); } } }); col[1].horizontal(|ui| { if ui.button("+").clicked() { self.add_value_dialog.show(); } if ui.button("-").clicked() { board.remove_label_value(selected_label_key, selected_label_value); *selected_label_value = String::new(); } }); }); if self.add_key_dialog.open { if let StringDialogResponse::Accepted(key) = self.add_key_dialog.ui(ui, "Add Key") { board.labels.insert(key, HashSet::new()); } } if self.add_value_dialog.open && selected_label_key != &String::new() { if let StringDialogResponse::Accepted(value) = self.add_value_dialog.ui(ui, "Add Value") { board.labels.get_mut(selected_label_key).unwrap().insert(value); } } }); } } #[derive(Default)] struct StringDialog { string: String, open: bool, } enum StringDialogResponse { Null, Cancelled, Accepted(String) } impl StringDialog { fn show(&mut self) { self.open = true; self.string = String::new(); } fn ui(&mut self, ui: &Ui, label: &str) -> StringDialogResponse { match Window::new(label) .collapsible(false) .show(ui.ctx(), |ui| { ui.text_edit_singleline(&mut self.string); (ui.button("Ok"), ui.button("Cancel")) }) { Some(InnerResponse { inner: Some((ok, cancel)), ..}) => { if ok.clicked() { self.open = false; StringDialogResponse::Accepted(self.string.clone()) } else if cancel.clicked() { self.open = false; StringDialogResponse::Cancelled } else { StringDialogResponse::Null } } _ => StringDialogResponse::Null, } } } fn node_common_ui(ui: &mut Ui, node: &mut Node, board: &Board) { ui.text_edit_singleline(&mut node.name); for (key, choices) in &board.labels { let choices = choices.clone(); let default = choices.iter().next().unwrap().clone(); let current = node.labels.entry(key.clone()).or_insert(default); ComboBox::from_label(key) .selected_text(current.to_string()) .show_ui(ui, |ui| { for choice in choices { ui.selectable_value(current, choice.clone(), choice); } }); } } #[derive(Default)] struct EditSettingsDialog { open: bool, board: Rc> } impl EditSettingsDialog { fn show(&mut self, board: Rc>) { self.board = board; self.open = true; } fn ui(&mut self, ctx: &Context) { Window::new("Settings") .collapsible(false) .open(&mut self.open) .show(ctx, |ui| { let mut board = self.board.borrow_mut(); ui.checkbox(&mut board.config.horizontal_wrapping, "Horizontal Wrapping"); }); } }