468 lines
16 KiB
Rust
468 lines
16 KiB
Rust
#![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<RefCell<Board>>,
|
|
texture: Option<TextureHandle>,
|
|
image: Option<image::DynamicImage>,
|
|
edit_mode: EditMode,
|
|
selected_node: Option<usize>,
|
|
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<Pos2> 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<RefCell<Board>>,
|
|
}
|
|
|
|
impl CreateNodeDialog {
|
|
fn show(&mut self, x: f32, y: f32, board: Rc<RefCell<Board>>) {
|
|
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<RefCell<Board>>,
|
|
}
|
|
|
|
impl EditNodeDialog {
|
|
fn show(&mut self, id: usize, board: Rc<RefCell<Board>>) {
|
|
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<RefCell<Board>>,
|
|
selected_label_key: String,
|
|
selected_label_value: String,
|
|
add_key_dialog: StringDialog,
|
|
add_value_dialog: StringDialog,
|
|
}
|
|
|
|
impl EditLabelsDialog {
|
|
fn show(&mut self, board: Rc<RefCell<Board>>) {
|
|
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<RefCell<Board>>
|
|
}
|
|
|
|
impl EditSettingsDialog {
|
|
fn show(&mut self, board: Rc<RefCell<Board>>) {
|
|
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");
|
|
});
|
|
}
|
|
}
|