From 9b0de108c8c57a1a2408056a3b7b1bb01796ddf5 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 16 Jan 2021 23:35:31 +0100 Subject: [PATCH] wip: node-graph --- Cargo.lock | 11 ++ egui/src/widgets/mod.rs | 11 ++ egui_demo_lib/Cargo.toml | 4 + egui_demo_lib/src/apps/mod.rs | 1 + egui_demo_lib/src/apps/node_graph.rs | 191 +++++++++++++++++++++++++++ egui_demo_lib/src/wrap_app.rs | 8 ++ 6 files changed, 226 insertions(+) create mode 100644 egui_demo_lib/src/apps/node_graph.rs diff --git a/Cargo.lock b/Cargo.lock index 015ce6b9..6c22ddd4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -661,6 +661,7 @@ dependencies = [ "epi", "image", "serde", + "slotmap", "syntect", ] @@ -1812,6 +1813,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" +[[package]] +name = "slotmap" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3003725ae562cf995f3dc82bb99e70926e09000396816765bb6d7adbe740b1" +dependencies = [ + "serde", + "version_check", +] + [[package]] name = "smallvec" version = "1.6.1" diff --git a/egui/src/widgets/mod.rs b/egui/src/widgets/mod.rs index 301f7372..71ac1f4a 100644 --- a/egui/src/widgets/mod.rs +++ b/egui/src/widgets/mod.rs @@ -48,6 +48,17 @@ pub fn reset_button(ui: &mut Ui, value: &mut T) { } } +/// Show a button to reset a value to its default. +/// The button is only enabled if the value does not already have its original value. +pub fn reset_button_with(ui: &mut Ui, value: &mut T, def: T) { + if ui + .add(Button::new("Reset").enabled(*value != def)) + .clicked() + { + *value = def; + } +} + // ---------------------------------------------------------------------------- pub fn stroke_ui(ui: &mut crate::Ui, stroke: &mut epaint::Stroke, text: &str) { diff --git a/egui_demo_lib/Cargo.toml b/egui_demo_lib/Cargo.toml index 647c1cb1..6b2a89fe 100644 --- a/egui_demo_lib/Cargo.toml +++ b/egui_demo_lib/Cargo.toml @@ -22,6 +22,10 @@ epi = { version = "0.9.0", path = "../epi" } image = { version = "0.23", default_features = false, features = ["jpeg", "png"], optional = true } syntect = { version = "4", default_features = false, features = ["default-fancy"], optional = true } # optional syntax highlighting +# feature "node_graph": +slotmap = { version = "1", default_features = false, features = ["serde"] } # TODO: optional +# petgraph = { version = "0.5", default_features = false, features = [] } # TODO: optional + # feature "persistence": serde = { version = "1", features = ["derive"], optional = true } diff --git a/egui_demo_lib/src/apps/mod.rs b/egui_demo_lib/src/apps/mod.rs index 0693c74c..3f995ade 100644 --- a/egui_demo_lib/src/apps/mod.rs +++ b/egui_demo_lib/src/apps/mod.rs @@ -4,6 +4,7 @@ mod easy_mark_editor; mod fractal_clock; #[cfg(feature = "http")] mod http_app; +pub mod node_graph; pub use color_test::ColorTest; pub use demo::DemoApp; diff --git a/egui_demo_lib/src/apps/node_graph.rs b/egui_demo_lib/src/apps/node_graph.rs new file mode 100644 index 00000000..aea58771 --- /dev/null +++ b/egui_demo_lib/src/apps/node_graph.rs @@ -0,0 +1,191 @@ +use egui::{containers::*, *}; + +// pub use petgraph::graph::{EdgeIndex as EdgeId, NodeIndex as NodeId}; +slotmap::new_key_type! { pub struct EdgeId; } +slotmap::new_key_type! { pub struct NodeId; } + +pub type Nodes = slotmap::SlotMap; +pub type Edges = slotmap::SlotMap; + +#[derive(Clone, Default)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "persistence", serde(default))] +pub struct NodeGraph { + // graph: petgraph::Graph, + nodes: Nodes, + edges: Edges, +} + +#[derive(Clone, PartialEq)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] +pub struct Edge { + nodes: [NodeId; 2], +} + +impl Edge { + pub fn new(from: NodeId, to: NodeId) -> Self { + Self { nodes: [from, to] } + } +} + +#[derive(Clone, PartialEq)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] +pub struct Node { + /// PERSISTED MODE: position is relative to parent! + rect: Rect, + title: String, + description: String, +} + +impl Default for Node { + fn default() -> Self { + Self { + rect: Rect::from_center_size(Pos2::ZERO, Vec2::splat(200.0)), + title: "unnamed".to_owned(), + description: Default::default(), + } + } +} + +impl Node { + pub fn new(title: impl Into, pos: impl Into) -> Self { + Self { + title: title.into(), + rect: Rect::from_center_size(pos.into(), Vec2::splat(100.0)), + ..Default::default() + } + } + + pub fn window(&mut self, id: NodeId, ui: &mut Ui) { + let translation = ui.min_rect().min - Pos2::ZERO; + + // TODO: set to use same layer as parent `ui` + let response = Window::new(self.title.clone()) + .id(egui::Id::new(id)) + .collapsible(false) + .scroll(false) + .resizable(true) + .default_size(self.rect.size()) + .title_bar(false) + .current_pos(self.rect.min + translation) + //.show_inside(ui, |ui|{ // TODO + .show(ui.ctx(), |ui| { + self.ui(ui); + }); + let response = response.expect("Window can't be closed"); + self.rect = response.rect.translate(-translation); + } + + pub fn ui(&mut self, ui: &mut Ui) { + let Self { + rect: _, + title, + description, + } = self; + + // Manual titlebar with editable title: + ui.vertical_centered(|ui| { + ui.add( + TextEdit::singleline(title) + .desired_width(32.0) + .text_style(TextStyle::Heading) + .frame(false), + ); + }); + ui.separator(); + + // Body: + ui.horizontal(|ui| { + ui.label("Description:"); + ui.text_edit_singleline(description); + }); + } +} + +impl Edge { + fn ui(&self, ui: &Ui, nodes: &Nodes) { + let translation = ui.min_rect().min - Pos2::ZERO; + + if let (Some(node0), Some(node1)) = (nodes.get(self.nodes[0]), nodes.get(self.nodes[1])) { + let rects = [ + node0.rect.translate(translation), + node1.rect.translate(translation), + ]; + + let x = lerp(rects[0].center().x..=rects[1].center().x, 0.5); + let y = lerp(rects[0].center().y..=rects[1].center().y, 0.5); + + let p0 = rects[0].clamp(pos2(x, y)); + let p1 = rects[1].clamp(pos2(x, y)); + + let stroke = Stroke::new(2.0, Color32::from_gray(200)); + ui.painter().arrow(p0, p1 - p0, stroke); + } + } +} + +impl NodeGraph { + fn egiu_deps() -> Self { + let mut graph = Self::default(); + let emath = graph.add_node(Node::new("emath", [200., 500.])); + let epaint = graph.add_node(Node::new("epaint", [200., 400.])); + let egui = graph.add_node(Node::new("egui", [200., 300.])); + + graph.add_edge(Edge::new(egui, emath)); + graph.add_edge(Edge::new(egui, epaint)); + + graph + } + + fn add_node(&mut self, node: Node) -> NodeId { + self.nodes.insert(node) + } + + fn add_edge(&mut self, edge: Edge) -> EdgeId { + self.edges.insert(edge) + } +} + +impl epi::App for NodeGraph { + fn name(&self) -> &str { + "Node Graph" + } + + #[cfg(feature = "persistence")] + fn load(&mut self, storage: &dyn epi::Storage) { + *self = epi::get_value(storage, "egui_demo_lib/apps/node_graph").unwrap_or_default() + } + + #[cfg(feature = "persistence")] + fn save(&mut self, storage: &mut dyn epi::Storage) { + epi::set_value(storage, "egui_demo_lib/apps/node_graph", self); + } + + fn update(&mut self, ctx: &egui::CtxRef, _frame: &mut epi::Frame<'_>) { + // TODO: side panel with "add windows" and whatnot + + egui::SidePanel::left("control_ui", 100.0).show(ctx, |ui| self.control_ui(ui)); + + egui::CentralPanel::default() + .frame(Frame::dark_canvas(&ctx.style())) + .show(ctx, |ui| self.contents_ui(ui)); + } +} + +impl NodeGraph { + pub fn control_ui(&mut self, ui: &mut Ui) { + // egui::reset_button_with(ui, self, Self::egiu_deps()); + if ui.button("Reset").clicked() { + *self = Self::egiu_deps(); + } + } + + pub fn contents_ui(&mut self, ui: &mut Ui) { + for (node_id, node) in &mut self.nodes { + node.window(node_id, ui); + } + for (_edge_id, edge) in &mut self.edges { + edge.ui(ui, &mut self.nodes); + } + } +} diff --git a/egui_demo_lib/src/wrap_app.rs b/egui_demo_lib/src/wrap_app.rs index b7fec40d..154d432d 100644 --- a/egui_demo_lib/src/wrap_app.rs +++ b/egui_demo_lib/src/wrap_app.rs @@ -9,6 +9,7 @@ pub struct Apps { http: crate::apps::HttpApp, clock: crate::apps::FractalClock, color_test: crate::apps::ColorTest, + node_graph: crate::apps::node_graph::NodeGraph, } impl Apps { @@ -20,6 +21,7 @@ impl Apps { ("http", &mut self.http as &mut dyn epi::App), ("clock", &mut self.clock as &mut dyn epi::App), ("colors", &mut self.color_test as &mut dyn epi::App), + ("node_graph", &mut self.node_graph as &mut dyn epi::App), ] .into_iter() } @@ -43,11 +45,17 @@ impl epi::App for WrapApp { #[cfg(feature = "persistence")] fn load(&mut self, storage: &dyn epi::Storage) { *self = epi::get_value(storage, epi::APP_KEY).unwrap_or_default() + // for (_, app) in self.iter_mut() { // less brittle! + // app.load(storage); // less brittle! + // } // less brittle! } #[cfg(feature = "persistence")] fn save(&mut self, storage: &mut dyn epi::Storage) { epi::set_value(storage, epi::APP_KEY, self); + // for (_, app) in self.iter_mut() { + // app.save(storage); + // } } fn warm_up_enabled(&self) -> bool {