From 4196f853e2cb8ce65d718eac90f6ab9ccd35fd48 Mon Sep 17 00:00:00 2001 From: starlord Date: Sun, 27 Oct 2024 19:30:35 +0200 Subject: [PATCH] Layouts (#218) * Default random layout * Possibility to provide custom layout * Hierarchical layout * User set positions prioritization Possibly fixes #219 --- Cargo.lock | 22 ++- Cargo.toml | 13 +- README.md | 44 +++--- examples/animated_nodes/src/node.rs | 6 +- examples/basic/src/main.rs | 18 ++- examples/basic_custom/Cargo.toml | 12 ++ examples/basic_custom/README.md | 7 + examples/basic_custom/src/main.rs | 49 +++++++ examples/demo/src/main.rs | 28 +--- examples/flex_nodes/src/main.rs | 29 ++-- examples/flex_nodes/src/node.rs | 6 +- examples/interactive/src/main.rs | 6 +- examples/label_change/src/main.rs | 4 +- examples/layouts/Cargo.toml | 12 ++ examples/layouts/README.md | 7 + examples/layouts/src/main.rs | 154 +++++++++++++++++++++ examples/multiple/src/main.rs | 8 +- examples/undirected/src/main.rs | 17 ++- examples/wasm_custom_draw/src/node.rs | 6 +- examples/window/src/main.rs | 6 +- src/draw/displays_default/edge.rs | 14 +- src/draw/displays_default/node.rs | 20 ++- src/draw/drawer.rs | 27 ++-- src/elements/edge.rs | 7 +- src/elements/node.rs | 48 ++++++- src/graph.rs | 57 ++++---- src/graph_view.rs | 102 ++++++++++---- src/helpers.rs | 191 ++++++++++++++------------ src/layouts/hierarchical/layout.rs | 113 +++++++++++++++ src/layouts/hierarchical/mod.rs | 3 + src/layouts/layout.rs | 27 ++++ src/layouts/mod.rs | 5 + src/layouts/random/layout.rs | 57 ++++++++ src/layouts/random/mod.rs | 3 + src/lib.rs | 9 +- src/metadata.rs | 45 +++--- 36 files changed, 871 insertions(+), 311 deletions(-) create mode 100644 examples/basic_custom/Cargo.toml create mode 100644 examples/basic_custom/README.md create mode 100644 examples/basic_custom/src/main.rs create mode 100644 examples/layouts/Cargo.toml create mode 100644 examples/layouts/README.md create mode 100644 examples/layouts/src/main.rs create mode 100644 src/layouts/hierarchical/layout.rs create mode 100644 src/layouts/hierarchical/mod.rs create mode 100644 src/layouts/layout.rs create mode 100644 src/layouts/mod.rs create mode 100644 src/layouts/random/layout.rs create mode 100644 src/layouts/random/mod.rs diff --git a/Cargo.lock b/Cargo.lock index ee62695..b124ecd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -650,6 +650,16 @@ dependencies = [ "petgraph", ] +[[package]] +name = "basic_custom" +version = "0.1.0" +dependencies = [ + "eframe", + "egui 0.29.0", + "egui_graphs 0.22.0", + "petgraph", +] + [[package]] name = "bevy" version = "0.14.2" @@ -2226,6 +2236,7 @@ dependencies = [ "epaint 0.29.0", "log", "nohash-hasher", + "ron", "serde", ] @@ -2303,7 +2314,6 @@ dependencies = [ "petgraph", "rand", "serde", - "serde_json", ] [[package]] @@ -3338,6 +3348,16 @@ dependencies = [ "petgraph", ] +[[package]] +name = "layouts" +version = "0.1.0" +dependencies = [ + "eframe", + "egui 0.29.0", + "egui_graphs 0.22.0", + "petgraph", +] + [[package]] name = "lazy_static" version = "1.5.0" diff --git a/Cargo.toml b/Cargo.toml index 5f5bbf0..942d442 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,20 +11,21 @@ keywords = ["egui", "ui", "graph", "node-graph"] categories = ["gui", "visualization"] [dependencies] -egui = { version = "0.29", default-features = false } +egui = { version = "0.29", default-features = false, features = [ + "persistence", +] } rand = "0.8" petgraph = { version = "0.6", default-features = false, features = [ "stable_graph", "matrix_graph", + "serde-1", ] } +serde = { version = "1.0", features = ["derive"] } + crossbeam = { version = "0.8", optional = true } -serde = { version = "1.0", features = ["derive"], optional = true } -serde_json = { version = "1.0", optional = true } [features] -egui_persistence = ["dep:serde"] -events = ["dep:serde", "dep:serde_json", "dep:crossbeam"] -serde = ["dep:serde", "egui/serde", "petgraph/serde", "petgraph/serde-1"] +events = ["dep:crossbeam"] [workspace] members = ["examples/*"] diff --git a/README.md b/README.md index 85ad534..27b6608 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The project implements a Widget for the egui framework, enabling easy visualizat - [x] Style configuration via egui context styles; - [x] Dark/Light theme support via egui context styles; - [x] Events reporting to extend the graph functionality by the user handling them; -- [ ] Layots and custom layout mechanism; +- [x] Layots and custom layout mechanism; ## Status The project is on track for a stable release v1.0.0. For the moment, breaking releases are very possible. @@ -25,19 +25,13 @@ Please use master branch for the latest updates. Check the [demo example](https://github.com/blitzarx1/egui_graphs/tree/master/examples/demo) for the comprehensive overview of the widget possibilities. -## Features -### Events -Can be enabled with `events` feature. Events describe a change made in graph whether it changed zoom level or node dragging. +## Layouts +In addition to the basic graph display functionality, the project provides a layout mechanism to arrange the nodes in the graph. The `Layout` trait can be implemented by the library user allowing for custom layouts. The following layouts are coming from the box: +- [x] Random layout; +- [x] Hierarchical layout; +- [ ] Force-directed layout; (coming soon) -Combining this feature with custom node draw function allows to implement custom node behavior and drawing according to the events happening. - -## Egui crates features support -### Persistence -To use egui `persistence` feature you need to enable `egui_persistence` feature of this crate. For example: -```toml -egui_graphs = { version = "0.22", features = ["egui_persistence"]} -egui = {version="0.29", features = ["persistence"]} -``` +Check the [layouts example](https://github.com/blitzarx1/egui_graphs/blob/master/examples/layouts/src/main.rs). ## Examples ### Basic setup example @@ -54,17 +48,18 @@ pub struct BasicApp { Next, implement the `new()` function for the `BasicApp` struct. ```rust impl BasicApp { - fn new(_: &CreationContext<'_>) -> Self { - Self { g: generate_graph() } + fn new(_: &eframe::CreationContext<'_>) -> Self { + let g = generate_graph(); + Self { g: egui_graphs::Graph::from(&g) } } } ``` #### Step 3: Generating the graph. -Create a helper function called `generate_graph()`. In this example, we create three nodes with and three edges connecting them in a triangular pattern. +Create a helper function called `generate_graph()`. In this example, we create three nodes and three edges. ```rust -fn generate_graph() -> egui_graphs::Graph { - let mut g = petgraph::stable_graph::StableGraph::new(); +fn generate_graph() -> petgraph::StableGraph<(), ()> { + let mut g = petgraph::StableGraph::new(); let a = g.add_node(()); let b = g.add_node(()); @@ -74,7 +69,7 @@ fn generate_graph() -> egui_graphs::Graph { g.add_edge(b, c, ()); g.add_edge(c, a, ()); - Graph::from(&g) + g } ``` @@ -91,13 +86,12 @@ impl eframe::App for BasicApp { ``` #### Step 5: Running the application. -Finally, run the application using the `eframe::run_native()` function with the specified native options and the `BasicApp`. +Finally, run the application using the `eframe::run_native()` function. ```rust fn main() { - let native_options = eframe::NativeOptions::default(); eframe::run_native( "egui_graphs_basic_demo", - native_options, + eframe::NativeOptions::default(), Box::new(|cc| Ok(Box::new(BasicApp::new(cc)))), ) .unwrap(); @@ -106,3 +100,9 @@ fn main() { ![Screenshot 2023-10-14 at 23 49 49](https://github.com/blitzarx1/egui_graphs/assets/32969427/584b78de-bca3-421b-b003-9321fd3e1b13) You can further customize the appearance and behavior of your graph by modifying the settings or adding more nodes and edges as needed. + +## Features +### Events +Can be enabled with `events` feature. Events describe a change made in graph whether it changed zoom level or node dragging. + +Combining this feature with custom node draw function allows to implement custom node behavior and drawing according to the events happening. diff --git a/examples/animated_nodes/src/node.rs b/examples/animated_nodes/src/node.rs index e38cd18..c8d93e4 100644 --- a/examples/animated_nodes/src/node.rs +++ b/examples/animated_nodes/src/node.rs @@ -61,8 +61,8 @@ impl NodeShapeAnimated { impl From> for NodeShapeAnimated { fn from(node_props: NodeProps) -> Self { Self { - label: node_props.label, - loc: node_props.location, + label: node_props.label.clone(), + loc: node_props.location(), dragged: node_props.dragged, clockwise: node_props.payload.get_is_clockwise(), @@ -145,7 +145,7 @@ impl DisplayNode< fn update(&mut self, state: &NodeProps) { self.label = state.label.clone(); - self.loc = state.location; + self.loc = state.location(); self.dragged = state.dragged; self.clockwise = state.payload.get_is_clockwise(); } diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs index 183a134..79ffe84 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/src/main.rs @@ -1,6 +1,6 @@ -use eframe::{run_native, App, CreationContext}; +use eframe::{run_native, App, CreationContext, NativeOptions}; use egui::Context; -use egui_graphs::{Graph, GraphView}; +use egui_graphs::{DefaultGraphView, Graph}; use petgraph::stable_graph::StableGraph; pub struct BasicApp { @@ -9,21 +9,20 @@ pub struct BasicApp { impl BasicApp { fn new(_: &CreationContext<'_>) -> Self { - Self { - g: generate_graph(), - } + let g = generate_graph(); + Self { g: Graph::from(&g) } } } impl App for BasicApp { fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { egui::CentralPanel::default().show(ctx, |ui| { - ui.add(&mut GraphView::new(&mut self.g)); + ui.add(&mut DefaultGraphView::new(&mut self.g)); }); } } -fn generate_graph() -> Graph { +fn generate_graph() -> StableGraph<(), ()> { let mut g = StableGraph::new(); let a = g.add_node(()); @@ -34,14 +33,13 @@ fn generate_graph() -> Graph { g.add_edge(b, c, ()); g.add_edge(c, a, ()); - Graph::from(&g) + g } fn main() { - let native_options = eframe::NativeOptions::default(); run_native( "egui_graphs_basic_demo", - native_options, + NativeOptions::default(), Box::new(|cc| Ok(Box::new(BasicApp::new(cc)))), ) .unwrap(); diff --git a/examples/basic_custom/Cargo.toml b/examples/basic_custom/Cargo.toml new file mode 100644 index 0000000..9dbdae3 --- /dev/null +++ b/examples/basic_custom/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "basic_custom" +version = "0.1.0" +authors = ["Dmitrii Samsonov "] +license = "MIT" +edition = "2021" + +[dependencies] +egui_graphs = { path = "../../" } +egui = "0.29" +eframe = "0.29" +petgraph = "0.6" diff --git a/examples/basic_custom/README.md b/examples/basic_custom/README.md new file mode 100644 index 0000000..48a17ed --- /dev/null +++ b/examples/basic_custom/README.md @@ -0,0 +1,7 @@ +# Basic +Basic example which demonstrates the usage of `GraphView` widget with helper functions. + +## run +```bash +cargo run --release -p basic_custom +``` \ No newline at end of file diff --git a/examples/basic_custom/src/main.rs b/examples/basic_custom/src/main.rs new file mode 100644 index 0000000..3a4f260 --- /dev/null +++ b/examples/basic_custom/src/main.rs @@ -0,0 +1,49 @@ +use eframe::{run_native, App, CreationContext, NativeOptions}; +use egui::{Context, Pos2}; +use egui_graphs::{DefaultGraphView, Graph, SettingsStyle}; +use petgraph::stable_graph::StableGraph; + +pub struct BasicCustomApp { + g: Graph, +} + +impl BasicCustomApp { + fn new(_: &CreationContext<'_>) -> Self { + let mut g = Graph::new(StableGraph::default()); + + let positions = vec![Pos2::new(0., 0.), Pos2::new(50., 0.), Pos2::new(0., 50.)]; + let mut idxs = Vec::with_capacity(positions.len()); + for position in positions { + let idx = g.add_node_with_label_and_location((), position.to_string(), position); + + idxs.push(idx); + } + + g.add_edge(idxs[0], idxs[1], ()); + g.add_edge(idxs[1], idxs[2], ()); + g.add_edge(idxs[2], idxs[0], ()); + + Self { g } + } +} + +impl App for BasicCustomApp { + fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + ui.add( + &mut DefaultGraphView::new(&mut self.g) + .with_styles(&SettingsStyle::default().with_labels_always(true)), + ); + }); + } +} + +fn main() { + let native_options = NativeOptions::default(); + run_native( + "egui_graphs_basic_custom_demo", + native_options, + Box::new(|cc| Ok(Box::new(BasicCustomApp::new(cc)))), + ) + .unwrap(); +} diff --git a/examples/demo/src/main.rs b/examples/demo/src/main.rs index 6f4a4d4..49dc394 100644 --- a/examples/demo/src/main.rs +++ b/examples/demo/src/main.rs @@ -5,11 +5,11 @@ use drawers::ValuesSectionDebug; use eframe::{run_native, App, CreationContext}; use egui::{CollapsingHeader, Context, Pos2, ScrollArea, Ui, Vec2}; use egui_graphs::events::Event; -use egui_graphs::{to_graph, Edge, Graph, GraphView, Node}; +use egui_graphs::{random_graph, DefaultGraphView, Edge, Graph, Node}; use fdg::fruchterman_reingold::{FruchtermanReingold, FruchtermanReingoldConfiguration}; use fdg::nalgebra::{Const, OPoint}; use fdg::{Force, ForceGraph}; -use petgraph::stable_graph::{DefaultIx, EdgeIndex, NodeIndex, StableGraph}; +use petgraph::stable_graph::{DefaultIx, EdgeIndex, NodeIndex}; use petgraph::Directed; use rand::Rng; @@ -50,7 +50,7 @@ impl DemoApp { let settings_graph = settings::SettingsGraph::default(); let settings_simulation = settings::SettingsSimulation::default(); - let mut g = generate_random_graph(settings_graph.count_node, settings_graph.count_edge); + let mut g = random_graph(settings_graph.count_node, settings_graph.count_edge); let mut force = init_force(&settings_simulation); let mut sim = fdg::init_force_graph_uniform(g.g.clone(), 1.0); @@ -447,7 +447,7 @@ impl DemoApp { let settings_graph = settings::SettingsGraph::default(); let settings_simulation = settings::SettingsSimulation::default(); - let mut g = generate_random_graph(settings_graph.count_node, settings_graph.count_edge); + let mut g = random_graph(settings_graph.count_node, settings_graph.count_edge); let mut force = init_force(&self.settings_simulation); let mut sim = fdg::init_force_graph_uniform(g.g.clone(), 1.0); @@ -511,7 +511,7 @@ impl App for DemoApp { let settings_style = &egui_graphs::SettingsStyle::new() .with_labels_always(self.settings_style.labels_always); ui.add( - &mut GraphView::new(&mut self.g) + &mut DefaultGraphView::new(&mut self.g) .with_interactions(settings_interaction) .with_navigations(settings_navigation) .with_styles(settings_style) @@ -526,24 +526,6 @@ impl App for DemoApp { } } -fn generate_random_graph(node_count: usize, edge_count: usize) -> Graph { - let mut rng = rand::thread_rng(); - let mut graph = StableGraph::new(); - - for _ in 0..node_count { - graph.add_node(()); - } - - for _ in 0..edge_count { - let source = rng.gen_range(0..node_count); - let target = rng.gen_range(0..node_count); - - graph.add_edge(NodeIndex::new(source), NodeIndex::new(target), ()); - } - - to_graph(&graph) -} - fn init_force(settings: &settings::SettingsSimulation) -> FruchtermanReingold { FruchtermanReingold { conf: FruchtermanReingoldConfiguration { diff --git a/examples/flex_nodes/src/main.rs b/examples/flex_nodes/src/main.rs index 1cd7511..56d8541 100644 --- a/examples/flex_nodes/src/main.rs +++ b/examples/flex_nodes/src/main.rs @@ -1,6 +1,6 @@ use eframe::{run_native, App, CreationContext}; use egui::{CentralPanel, Context, SidePanel, TextEdit}; -use egui_graphs::{DefaultEdgeShape, Graph, GraphView, SettingsInteraction, SettingsNavigation}; +use egui_graphs::{Graph, GraphView, SettingsInteraction, SettingsNavigation}; use node::NodeShapeFlex; use petgraph::{ stable_graph::{DefaultIx, EdgeIndex, NodeIndex, StableGraph}, @@ -10,7 +10,7 @@ use petgraph::{ mod node; pub struct FlexNodesApp { - g: Graph<(), (), Directed, DefaultIx, NodeShapeFlex, DefaultEdgeShape>, + g: Graph<(), (), Directed, DefaultIx, NodeShapeFlex>, label_input: String, selected_node: Option, selected_edge: Option, @@ -58,19 +58,18 @@ impl FlexNodesApp { } }); CentralPanel::default().show(ctx, |ui| { - let widget = - &mut GraphView::<_, _, _, _, NodeShapeFlex, DefaultEdgeShape>::new(&mut self.g) - .with_interactions( - &SettingsInteraction::default() - .with_dragging_enabled(true) - .with_node_selection_enabled(true) - .with_edge_selection_enabled(true), - ) - .with_navigations( - &SettingsNavigation::default() - .with_fit_to_screen_enabled(false) - .with_zoom_and_pan_enabled(true), - ); + let widget = &mut GraphView::<_, _, _, _, _, _>::new(&mut self.g) + .with_interactions( + &SettingsInteraction::default() + .with_dragging_enabled(true) + .with_node_selection_enabled(true) + .with_edge_selection_enabled(true), + ) + .with_navigations( + &SettingsNavigation::default() + .with_fit_to_screen_enabled(false) + .with_zoom_and_pan_enabled(true), + ); ui.add(widget); }); } diff --git a/examples/flex_nodes/src/node.rs b/examples/flex_nodes/src/node.rs index d52e9a7..3470640 100644 --- a/examples/flex_nodes/src/node.rs +++ b/examples/flex_nodes/src/node.rs @@ -14,8 +14,8 @@ pub struct NodeShapeFlex { impl From> for NodeShapeFlex { fn from(node_props: NodeProps) -> Self { Self { - label: node_props.label, - loc: node_props.location, + label: node_props.label.clone(), + loc: node_props.location(), size_x: 0., size_y: 0., @@ -67,7 +67,7 @@ impl DisplayNode fn update(&mut self, state: &NodeProps) { self.label = state.label.clone(); - self.loc = state.location; + self.loc = state.location(); } } diff --git a/examples/interactive/src/main.rs b/examples/interactive/src/main.rs index 8dbf120..f92e070 100644 --- a/examples/interactive/src/main.rs +++ b/examples/interactive/src/main.rs @@ -1,6 +1,6 @@ use eframe::{run_native, App, CreationContext}; use egui::Context; -use egui_graphs::{Graph, GraphView, SettingsInteraction, SettingsStyle}; +use egui_graphs::{DefaultGraphView, Graph, SettingsInteraction, SettingsStyle}; use petgraph::stable_graph::StableGraph; pub struct InteractiveApp { @@ -9,7 +9,7 @@ pub struct InteractiveApp { impl InteractiveApp { fn new(_: &CreationContext<'_>) -> Self { - let g = generate_graph(); + let g: Graph = generate_graph(); Self { g } } } @@ -27,7 +27,7 @@ impl App for InteractiveApp { .with_edge_selection_multi_enabled(true); let style_settings = &SettingsStyle::new().with_labels_always(true); ui.add( - &mut GraphView::new(&mut self.g) + &mut DefaultGraphView::new(&mut self.g) .with_styles(style_settings) .with_interactions(interaction_settings), ); diff --git a/examples/label_change/src/main.rs b/examples/label_change/src/main.rs index acd8666..ec42fef 100644 --- a/examples/label_change/src/main.rs +++ b/examples/label_change/src/main.rs @@ -1,6 +1,6 @@ use eframe::{run_native, App, CreationContext}; use egui::{CentralPanel, Context, SidePanel, TextEdit}; -use egui_graphs::{Graph, GraphView, SettingsInteraction, SettingsStyle}; +use egui_graphs::{DefaultGraphView, Graph, SettingsInteraction, SettingsStyle}; use petgraph::stable_graph::{EdgeIndex, NodeIndex, StableGraph}; pub struct LabelChangeApp { @@ -52,7 +52,7 @@ impl LabelChangeApp { } }); CentralPanel::default().show(ctx, |ui| { - let widget = &mut GraphView::new(&mut self.g) + let widget = &mut DefaultGraphView::new(&mut self.g) .with_interactions( &SettingsInteraction::default() .with_node_selection_enabled(true) diff --git a/examples/layouts/Cargo.toml b/examples/layouts/Cargo.toml new file mode 100644 index 0000000..ffa91b3 --- /dev/null +++ b/examples/layouts/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "layouts" +version = "0.1.0" +authors = ["Dmitrii Samsonov "] +license = "MIT" +edition = "2021" + +[dependencies] +egui_graphs = { path = "../../" } +egui = "0.29" +eframe = "0.29" +petgraph = "0.6" diff --git a/examples/layouts/README.md b/examples/layouts/README.md new file mode 100644 index 0000000..17210be --- /dev/null +++ b/examples/layouts/README.md @@ -0,0 +1,7 @@ +# Layouts +Example of usage of different lyouts implemented by `egui_graphs`. + +## run +```bash +cargo run --release -p layouts +``` \ No newline at end of file diff --git a/examples/layouts/src/main.rs b/examples/layouts/src/main.rs new file mode 100644 index 0000000..c4fa0a0 --- /dev/null +++ b/examples/layouts/src/main.rs @@ -0,0 +1,154 @@ +use eframe::{run_native, App, CreationContext, NativeOptions}; +use egui::Context; +use egui_graphs::{ + random_graph, DefaultEdgeShape, DefaultNodeShape, Graph, GraphView, LayoutHierarchical, + LayoutRandom, LayoutStateHierarchical, LayoutStateRandom, +}; +use petgraph::{stable_graph::DefaultIx, Directed}; + +#[derive(Clone, PartialEq)] +enum Layout { + Hierarchical, + Random, +} + +#[derive(Clone)] +struct Settings { + layout: Layout, + num_nodes: usize, + num_edges: usize, +} +pub struct LayoutsApp { + settings: Settings, + g: Graph, +} + +impl LayoutsApp { + fn new(_: &CreationContext<'_>) -> Self { + let settings = Settings { + layout: Layout::Hierarchical, + num_nodes: 25, + num_edges: 25, + }; + Self { + settings: settings.clone(), + g: random_graph(settings.num_nodes, settings.num_edges), + } + } + + fn clear_cache(&mut self, ui: &mut egui::Ui) { + match self.settings.layout { + Layout::Hierarchical => { + GraphView::< + (), + (), + Directed, + DefaultIx, + DefaultNodeShape, + DefaultEdgeShape, + LayoutStateHierarchical, + LayoutHierarchical, + >::clear_cache(ui); + } + Layout::Random => { + GraphView::< + (), + (), + Directed, + DefaultIx, + DefaultNodeShape, + DefaultEdgeShape, + LayoutStateRandom, + LayoutRandom, + >::clear_cache(ui); + } + }; + } +} + +impl App for LayoutsApp { + fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { + egui::SidePanel::right("right_panel") + .min_width(250.) + .show(ctx, |ui| { + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.label("Layout"); + if ui + .radio_value( + &mut self.settings.layout, + Layout::Hierarchical, + "Hierarchical", + ) + .changed() + { + self.clear_cache(ui); + }; + if ui + .radio_value(&mut self.settings.layout, Layout::Random, "Random") + .changed() + { + self.clear_cache(ui); + }; + }); + ui.horizontal(|ui| { + ui.label("Number of nodes"); + if ui + .add(egui::Slider::new(&mut self.settings.num_nodes, 1..=250)) + .changed() + { + self.clear_cache(ui); + self.g = random_graph(self.settings.num_nodes, self.settings.num_edges); + }; + }); + ui.horizontal(|ui| { + ui.label("Number of edges"); + if ui + .add(egui::Slider::new(&mut self.settings.num_edges, 1..=250)) + .changed() + { + self.clear_cache(ui); + self.g = random_graph(self.settings.num_nodes, self.settings.num_edges); + }; + }); + }); + }); + egui::CentralPanel::default().show(ctx, |ui| { + match self.settings.layout { + Layout::Hierarchical => { + ui.add(&mut GraphView::< + _, + _, + _, + _, + _, + _, + LayoutStateHierarchical, + LayoutHierarchical, + >::new(&mut self.g)); + } + Layout::Random => { + ui.add(&mut GraphView::< + _, + _, + _, + _, + _, + _, + LayoutStateRandom, + LayoutRandom, + >::new(&mut self.g)); + } + }; + }); + } +} + +fn main() { + run_native( + "egui_graphs_layouts_demo", + NativeOptions::default(), + Box::new(|cc| Ok(Box::new(LayoutsApp::new(cc)))), + ) + .unwrap(); +} diff --git a/examples/multiple/src/main.rs b/examples/multiple/src/main.rs index 5ceec53..95d3eb4 100644 --- a/examples/multiple/src/main.rs +++ b/examples/multiple/src/main.rs @@ -1,6 +1,6 @@ use eframe::{run_native, App, CreationContext, Frame}; use egui::{CentralPanel, Context, Layout, SidePanel}; -use egui_graphs::{Graph, GraphView, SettingsInteraction, SettingsNavigation}; +use egui_graphs::{DefaultGraphView, Graph, SettingsInteraction, SettingsNavigation}; use petgraph::stable_graph::StableGraph; pub struct BasicApp { @@ -23,7 +23,7 @@ impl App for BasicApp { .show(ctx, |ui| { ui.allocate_ui_with_layout(ui.max_rect().size(), Layout::default(), |ui| { ui.add( - &mut GraphView::new(&mut self.g) + &mut DefaultGraphView::new(&mut self.g) .with_navigations( &SettingsNavigation::default() .with_fit_to_screen_enabled(false) @@ -45,7 +45,7 @@ impl App for BasicApp { .resizable(true) .show(ctx, |ui| { ui.add( - &mut GraphView::new(&mut self.g) + &mut DefaultGraphView::new(&mut self.g) .with_navigations( &SettingsNavigation::default() .with_fit_to_screen_enabled(false) @@ -63,7 +63,7 @@ impl App for BasicApp { }); CentralPanel::default().show(ctx, |ui| { ui.add( - &mut GraphView::new(&mut self.g) + &mut DefaultGraphView::new(&mut self.g) .with_navigations( &SettingsNavigation::default() .with_fit_to_screen_enabled(false) diff --git a/examples/undirected/src/main.rs b/examples/undirected/src/main.rs index 1196710..d4c10a1 100644 --- a/examples/undirected/src/main.rs +++ b/examples/undirected/src/main.rs @@ -1,13 +1,13 @@ use eframe::{run_native, App, CreationContext}; use egui::Context; -use egui_graphs::{Graph, GraphView}; +use egui_graphs::{Graph, GraphView, LayoutRandom, LayoutStateRandom}; use petgraph::{ - stable_graph::{DefaultIx, StableGraph, StableUnGraph}, + stable_graph::{StableGraph, StableUnGraph}, Undirected, }; pub struct UndirectedApp { - g: Graph<(), (), Undirected, DefaultIx>, + g: Graph<(), (), Undirected>, } impl UndirectedApp { @@ -20,7 +20,16 @@ impl UndirectedApp { impl App for UndirectedApp { fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { egui::CentralPanel::default().show(ctx, |ui| { - ui.add(&mut GraphView::new(&mut self.g)); + ui.add(&mut GraphView::< + _, + _, + _, + _, + _, + _, + LayoutStateRandom, + LayoutRandom, + >::new(&mut self.g)); }); } } diff --git a/examples/wasm_custom_draw/src/node.rs b/examples/wasm_custom_draw/src/node.rs index fb3d337..636e4a1 100644 --- a/examples/wasm_custom_draw/src/node.rs +++ b/examples/wasm_custom_draw/src/node.rs @@ -24,8 +24,8 @@ pub struct NodeShapeAnimated { impl From> for NodeShapeAnimated { fn from(node_props: NodeProps) -> Self { Self { - label: node_props.label, - loc: node_props.location, + label: node_props.label.clone(), + loc: node_props.location(), dragged: node_props.dragged, angle_rad: Default::default(), @@ -120,7 +120,7 @@ impl DisplayNode fn update(&mut self, state: &NodeProps) { self.label = state.label.clone(); - self.loc = state.location; + self.loc = state.location(); self.dragged = state.dragged; } } diff --git a/examples/window/src/main.rs b/examples/window/src/main.rs index 5ba2ac3..cae4568 100644 --- a/examples/window/src/main.rs +++ b/examples/window/src/main.rs @@ -1,10 +1,10 @@ use eframe::{run_native, App, CreationContext}; use egui::{Context, Window}; -use egui_graphs::{Graph, GraphView}; +use egui_graphs::{DefaultGraphView, Graph}; use petgraph::stable_graph::StableGraph; pub struct WindowApp { - g: Graph<(), ()>, + g: Graph, } impl WindowApp { @@ -17,7 +17,7 @@ impl WindowApp { impl App for WindowApp { fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { Window::new("graph").show(ctx, |ui| { - ui.add(&mut GraphView::new(&mut self.g)); + ui.add(&mut DefaultGraphView::new(&mut self.g)); }); } } diff --git a/src/draw/displays_default/edge.rs b/src/draw/displays_default/edge.rs index 65ee1c9..82e95fc 100644 --- a/src/draw/displays_default/edge.rs +++ b/src/draw/displays_default/edge.rs @@ -6,11 +6,10 @@ use egui::{ }; use petgraph::{stable_graph::IndexType, EdgeType}; -use crate::{draw::DrawContext, elements::EdgeProps, DisplayEdge, DisplayNode, Node}; +use crate::{draw::DrawContext, elements::EdgeProps, node_size, DisplayEdge, DisplayNode, Node}; use super::edge_shape_builder::{EdgeShapeBuilder, TipProps}; -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug)] pub struct DefaultEdgeShape { pub order: usize, @@ -275,17 +274,6 @@ impl DefaultEdgeShape { } } -// TODO: export this func as common drawing func -fn node_size>( - node: &Node, - dir: Vec2, -) -> f32 { - let connector_left = node.display().closest_boundary_point(dir); - let connector_right = node.display().closest_boundary_point(-dir); - - ((connector_right.to_vec2() - connector_left.to_vec2()) / 2.).length() -} - /// Returns the distance from line segment `a``b` to point `c`. /// Adapted from fn distance_segment_to_point(a: Pos2, b: Pos2, point: Pos2) -> f32 { diff --git a/src/draw/displays_default/node.rs b/src/draw/displays_default/node.rs index b354509..b6c032a 100644 --- a/src/draw/displays_default/node.rs +++ b/src/draw/displays_default/node.rs @@ -1,6 +1,6 @@ use egui::{ epaint::{CircleShape, TextShape}, - FontFamily, FontId, Pos2, Shape, Stroke, Vec2, + Color32, FontFamily, FontId, Pos2, Shape, Stroke, Vec2, }; use petgraph::{stable_graph::IndexType, EdgeType}; @@ -9,27 +9,28 @@ use crate::{draw::drawer::DrawContext, DisplayNode, NodeProps}; /// This is the default node shape which is used to display nodes in the graph. /// /// You can use this implementation as an example for implementing your own custom node shapes. -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug)] pub struct DefaultNodeShape { pub pos: Pos2, pub selected: bool, pub dragged: bool, + pub color: Option, pub label_text: String, - /// Shape defined property + /// Shape dependent property pub radius: f32, } impl From> for DefaultNodeShape { fn from(node_props: NodeProps) -> Self { DefaultNodeShape { - pos: node_props.location, + pos: node_props.location(), selected: node_props.selected, dragged: node_props.dragged, label_text: node_props.label.to_string(), + color: node_props.color(), radius: 5.0, } @@ -57,7 +58,12 @@ impl DisplayNode } else { ctx.ctx.style().visuals.widgets.inactive }; - let color = style.fg_stroke.color; + + let color = if let Some(c) = self.color { + c + } else { + style.fg_stroke.color + }; let circle_center = ctx.meta.canvas_to_screen_pos(self.pos); let circle_radius = ctx.meta.canvas_to_screen_size(self.radius); @@ -95,11 +101,11 @@ impl DisplayNode } fn update(&mut self, state: &NodeProps) { - self.pos = state.location; - self.pos = state.location; + self.pos = state.location(); self.selected = state.selected; self.dragged = state.dragged; self.label_text = state.label.to_string(); + self.color = state.color(); } } diff --git a/src/draw/drawer.rs b/src/draw/drawer.rs index 18e028b..f7be370 100644 --- a/src/draw/drawer.rs +++ b/src/draw/drawer.rs @@ -4,7 +4,11 @@ use egui::{Context, Painter, Shape}; use petgraph::graph::IndexType; use petgraph::EdgeType; -use crate::{settings::SettingsStyle, Graph, Metadata}; +use crate::{ + layouts::{Layout, LayoutState}, + settings::SettingsStyle, + Graph, Metadata, +}; use super::{DisplayEdge, DisplayNode}; @@ -17,7 +21,7 @@ pub struct DrawContext<'a> { pub meta: &'a Metadata, } -pub struct Drawer<'a, N, E, Ty, Ix, Nd, Ed> +pub struct Drawer<'a, N, E, Ty, Ix, Nd, Ed, S, L> where N: Clone, E: Clone, @@ -25,14 +29,17 @@ where Ix: IndexType, Nd: DisplayNode, Ed: DisplayEdge, + S: LayoutState, + L: Layout, { ctx: &'a DrawContext<'a>, g: &'a mut Graph, - postponed: Vec, - _marker: PhantomData<(Nd, Ed)>, + delayed: Vec, + + _marker: PhantomData<(Nd, Ed, L, S)>, } -impl<'a, N, E, Ty, Ix, Nd, Ed> Drawer<'a, N, E, Ty, Ix, Nd, Ed> +impl<'a, N, E, Ty, Ix, Nd, Ed, S, L> Drawer<'a, N, E, Ty, Ix, Nd, Ed, S, L> where N: Clone, E: Clone, @@ -40,12 +47,14 @@ where Ix: IndexType, Nd: DisplayNode, Ed: DisplayEdge, + S: LayoutState, + L: Layout, { pub fn new(g: &'a mut Graph, ctx: &'a DrawContext<'a>) -> Self { Drawer { ctx, g, - postponed: Vec::new(), + delayed: Vec::new(), _marker: PhantomData, } } @@ -57,7 +66,7 @@ where } fn draw_postponed(&mut self) { - self.postponed.iter().for_each(|s| { + self.delayed.iter().for_each(|s| { self.ctx.painter.add(s.clone()); }); } @@ -78,7 +87,7 @@ where if n.selected() || n.dragged() { for s in shapes { - self.postponed.push(s); + self.delayed.push(s); } } else { for s in shapes { @@ -110,7 +119,7 @@ where if e.selected() { for s in shapes { - self.postponed.push(s); + self.delayed.push(s); } } else { for s in shapes { diff --git a/src/elements/edge.rs b/src/elements/edge.rs index b098687..882c366 100644 --- a/src/elements/edge.rs +++ b/src/elements/edge.rs @@ -4,12 +4,12 @@ use petgraph::{ stable_graph::{DefaultIx, EdgeIndex, IndexType}, Directed, EdgeType, }; +use serde::{Deserialize, Serialize}; use crate::{DefaultEdgeShape, DefaultNodeShape, DisplayEdge, DisplayNode}; /// Stores properties of an [Edge] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct EdgeProps { pub payload: E, pub order: usize, @@ -18,8 +18,7 @@ pub struct EdgeProps { } /// Stores properties of an edge that can be changed. Used to apply changes to the graph. -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Edge< N: Clone, E: Clone, diff --git a/src/elements/node.rs b/src/elements/node.rs index bded2b2..ee114b6 100644 --- a/src/elements/node.rs +++ b/src/elements/node.rs @@ -1,26 +1,45 @@ use std::fmt::Debug; use std::marker::PhantomData; -use egui::Pos2; +use egui::{Color32, Pos2}; use petgraph::{ stable_graph::{DefaultIx, IndexType, NodeIndex}, Directed, EdgeType, }; +use serde::{Deserialize, Serialize}; use crate::{DefaultNodeShape, DisplayNode}; /// Stores properties of a [Node] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug)] -pub struct NodeProps { +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct NodeProps +where + N: Clone, +{ pub payload: N, - pub location: Pos2, pub label: String, pub selected: bool, pub dragged: bool, + + color: Option, + location: Pos2, + location_user: Option, } -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +impl NodeProps +where + N: Clone, +{ + pub fn location(&self) -> Pos2 { + self.location_user.unwrap_or(self.location) + } + + pub fn color(&self) -> Option { + self.color + } +} + +#[derive(Serialize, Deserialize)] pub struct Node where N: Clone, @@ -83,6 +102,8 @@ where let props = NodeProps { payload, location: Pos2::default(), + color: Option::default(), + location_user: Option::default(), label: String::default(), selected: bool::default(), dragged: bool::default(), @@ -132,11 +153,24 @@ where &mut self.props.payload } + pub fn color(&self) -> Option { + self.props.color() + } + + pub fn set_color(&mut self, color: Color32) { + self.props.color = Some(color); + } + pub fn location(&self) -> Pos2 { - self.props.location + self.props.location() } pub fn set_location(&mut self, loc: Pos2) { + self.props.location_user = Some(loc); + } + + // TODO: why crate? how to use by external layoyuts?? do we need this func??? + pub(crate) fn set_layout_location(&mut self, loc: Pos2) { self.props.location = loc; } diff --git a/src/graph.rs b/src/graph.rs index 8673067..93c549f 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -8,6 +8,7 @@ use petgraph::{ visit::{EdgeRef, IntoEdgeReferences, IntoNodeReferences}, Direction, EdgeType, }; +use serde::{Deserialize, Serialize}; use crate::draw::{DisplayEdge, DisplayNode}; use crate::{metadata::Metadata, Edge, Node}; @@ -18,44 +19,50 @@ type StableGraphType = /// Wrapper around [`petgraph::stable_graph::StableGraph`] compatible with [`super::GraphView`]. /// It is used to store graph data and provide access to it. -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Graph< - N: Clone = (), - E: Clone = (), - Ty: EdgeType = Directed, - Ix: IndexType = DefaultIx, - Dn: DisplayNode = DefaultNodeShape, - De: DisplayEdge = DefaultEdgeShape, -> { + N = (), + E = (), + Ty = Directed, + Ix = DefaultIx, + Dn = DefaultNodeShape, + De = DefaultEdgeShape, +> where + N: Clone, + E: Clone, + Ty: EdgeType, + Ix: IndexType, + Dn: DisplayNode, + De: DisplayEdge, +{ pub g: StableGraphType, selected_nodes: Vec>, selected_edges: Vec>, dragged_node: Option>, } -impl< - N: Clone, - E: Clone, - Ty: EdgeType, - Ix: IndexType, - Dn: DisplayNode, - De: DisplayEdge, - > From<&StableGraph> for Graph +impl From<&StableGraph> for Graph +where + N: Clone, + E: Clone, + Ty: EdgeType, + Ix: IndexType, + Dn: DisplayNode, + De: DisplayEdge, { fn from(g: &StableGraph) -> Self { to_graph(g) } } -impl< - N: Clone, - E: Clone, - Ty: EdgeType, - Ix: IndexType, - Dn: DisplayNode, - De: DisplayEdge, - > Graph +impl Graph +where + N: Clone, + E: Clone, + Ty: EdgeType, + Ix: IndexType, + Dn: DisplayNode, + De: DisplayEdge, { pub fn new(g: StableGraphType) -> Self { Self { diff --git a/src/graph_view.rs b/src/graph_view.rs index 53faa28..6067d30 100644 --- a/src/graph_view.rs +++ b/src/graph_view.rs @@ -1,28 +1,41 @@ use std::marker::PhantomData; +use crate::{ + draw::{DefaultEdgeShape, DefaultNodeShape, DrawContext, Drawer}, + layouts::{self, Layout, LayoutState}, + metadata::Metadata, + settings::{SettingsInteraction, SettingsNavigation, SettingsStyle}, + DisplayEdge, DisplayNode, Graph, +}; + +use egui::{Id, PointerButton, Pos2, Rect, Response, Sense, Ui, Vec2, Widget}; + +use petgraph::{graph::EdgeIndex, stable_graph::DefaultIx}; +use petgraph::{graph::IndexType, Directed}; +use petgraph::{stable_graph::NodeIndex, EdgeType}; + +const KEY_LAYOUT: &str = "egui_grpahs_layout"; + +pub type DefaultGraphView<'a> = GraphView< + 'a, + (), + (), + Directed, + DefaultIx, + DefaultNodeShape, + DefaultEdgeShape, + layouts::random::State, + layouts::random::Random, +>; + #[cfg(feature = "events")] use crate::events::{ Event, PayloadEdgeClick, PayloadEdgeDeselect, PayloadEdgeSelect, PayloadNodeClick, PayloadNodeDeselect, PayloadNodeDoubleClick, PayloadNodeDragEnd, PayloadNodeDragStart, PayloadNodeMove, PayloadNodeSelect, PayloadPan, PayloadZoom, }; -use crate::{ - draw::Drawer, - metadata::Metadata, - settings::SettingsNavigation, - settings::{SettingsInteraction, SettingsStyle}, - Graph, -}; -use crate::{ - draw::{DefaultEdgeShape, DefaultNodeShape, DrawContext}, - DisplayEdge, DisplayNode, -}; #[cfg(feature = "events")] use crossbeam::channel::Sender; -use egui::{PointerButton, Pos2, Rect, Response, Sense, Ui, Vec2, Widget}; -use petgraph::{graph::EdgeIndex, stable_graph::DefaultIx}; -use petgraph::{graph::IndexType, Directed}; -use petgraph::{stable_graph::NodeIndex, EdgeType}; /// Widget for visualizing and interacting with graphs. /// @@ -32,8 +45,8 @@ use petgraph::{stable_graph::NodeIndex, EdgeType}; /// struct to visualize and interact with the graph. `N` and `E` is arbitrary client data associated with nodes and edges. /// You can customize the visualization and interaction behavior using [`SettingsInteraction`], [`SettingsNavigation`] and [`SettingsStyle`] structs. /// -/// When any interaction or node property change occurs, the widget sends [Event] struct to the provided -/// [Sender] channel, which can be set via the `with_interactions` method. The [Event] struct contains information about +/// When any interaction or node property change occurs, the widget sends [`Event`] struct to the provided +/// [`Sender`] channel, which can be set via the `with_interactions` method. The [`Event`] struct contains information about /// a change that occurred in the graph. Client can use this information to modify external state of his application if needed. /// /// When the user performs navigation actions (zoom & pan or fit to screen), they do not @@ -47,6 +60,8 @@ pub struct GraphView< Ix = DefaultIx, Nd = DefaultNodeShape, Ed = DefaultEdgeShape, + S = layouts::random::State, + L = layouts::random::Random, > where N: Clone, E: Clone, @@ -54,6 +69,8 @@ pub struct GraphView< Ix: IndexType, Nd: DisplayNode, Ed: DisplayEdge, + S: LayoutState, + L: Layout, { g: &'a mut Graph, @@ -64,10 +81,10 @@ pub struct GraphView< #[cfg(feature = "events")] events_publisher: Option<&'a Sender>, - _marker: PhantomData<(Nd, Ed)>, + _marker: PhantomData<(Nd, Ed, L, S)>, } -impl<'a, N, E, Ty, Ix, Nd, Ed> Widget for &mut GraphView<'a, N, E, Ty, Ix, Nd, Ed> +impl<'a, N, E, Ty, Ix, Nd, Ed, S, L> Widget for &mut GraphView<'a, N, E, Ty, Ix, Nd, Ed, S, L> where N: Clone, E: Clone, @@ -75,41 +92,43 @@ where Ix: IndexType, Nd: DisplayNode, Ed: DisplayEdge, + S: LayoutState, + L: Layout, { fn ui(self, ui: &mut Ui) -> Response { - let (resp, p) = ui.allocate_painter(ui.available_size(), Sense::click_and_drag()); + self.sync_layout(ui); - let mut meta = Metadata::get(ui); + let mut meta = Metadata::load(ui); self.sync_state(&mut meta); + let (resp, p) = ui.allocate_painter(ui.available_size(), Sense::click_and_drag()); self.handle_fit_to_screen(&resp, &mut meta); self.handle_navigation(ui, &resp, &mut meta); - self.handle_node_drag(&resp, &mut meta); self.handle_click(&resp, &mut meta); - let is_directed = self.g.is_directed(); - Drawer::::new( + Drawer::::new( self.g, &DrawContext { ctx: ui.ctx(), painter: &p, meta: &meta, - is_directed, + is_directed: self.g.is_directed(), style: &self.settings_style, }, ) .draw(); meta.first_frame = false; - meta.store_into_ui(ui); + meta.save(ui); + ui.ctx().request_repaint(); resp } } -impl<'a, N, E, Ty, Ix, Dn, De> GraphView<'a, N, E, Ty, Ix, Dn, De> +impl<'a, N, E, Ty, Ix, Dn, De, S, L> GraphView<'a, N, E, Ty, Ix, Dn, De, S, L> where N: Clone, E: Clone, @@ -117,6 +136,8 @@ where Ix: IndexType, Dn: DisplayNode, De: DisplayEdge, + S: LayoutState, + L: Layout, { /// Creates a new `GraphView` widget with default navigation and interactions settings. /// To customize navigation and interactions use `with_interactions` and `with_navigations` methods. @@ -153,9 +174,22 @@ where self } + /// Clears cached values of layout and metadata. + pub fn clear_cache(ui: &mut Ui) { + GraphView::::reset_metadata(ui); + GraphView::::reset_layout(ui); + } + /// Resets navigation metadata pub fn reset_metadata(ui: &mut Ui) { - Metadata::default().store_into_ui(ui); + Metadata::default().save(ui); + } + + /// Resets layout state + pub fn reset_layout(ui: &mut Ui) { + ui.data_mut(|data| { + data.insert_persisted(Id::new(KEY_LAYOUT), S::default()); + }); } #[cfg(feature = "events")] @@ -165,6 +199,18 @@ where self } + fn sync_layout(&mut self, ui: &mut Ui) { + ui.data_mut(|data| { + let state = data + .get_persisted::(Id::new(KEY_LAYOUT)) + .unwrap_or_default(); + let mut layout = L::from_state(state); + layout.next(self.g); + + data.insert_persisted(Id::new(KEY_LAYOUT), layout.state()); + }); + } + fn sync_state(&mut self, meta: &mut Metadata) { let mut selected_nodes = Vec::new(); let mut selected_edges = Vec::new(); diff --git a/src/helpers.rs b/src/helpers.rs index 3488cb3..aefb5b9 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,5 +1,5 @@ use crate::{DisplayEdge, DisplayNode, Edge, Graph, Node}; -use egui::Pos2; +use egui::Vec2; use petgraph::{ graph::IndexType, stable_graph::{EdgeIndex, NodeIndex, StableGraph}, @@ -9,32 +9,37 @@ use petgraph::{ use rand::Rng; use std::collections::HashMap; -pub const DEFAULT_SPAWN_SIZE: f32 = 250.; - /// Helper function which adds user's node to the [`super::Graph`] instance. /// /// If graph is not empty it picks any node position and adds new node in the vicinity of it. -pub fn add_node>( - g: &mut Graph, - n: &N, -) -> NodeIndex { +pub fn add_node(g: &mut Graph, n: &N) -> NodeIndex +where + N: Clone, + E: Clone, + Ty: EdgeType, + Ix: IndexType, + Dn: DisplayNode, + De: DisplayEdge, +{ add_node_custom(g, n, default_node_transform) } /// Helper function which adds user's node to the [`super::Graph`] instance with custom node transform function. /// /// If graph is not empty it picks any node position and adds new node in the vicinity of it. -pub fn add_node_custom< +pub fn add_node_custom( + g: &mut Graph, + n: &N, + node_transform: impl FnOnce(&mut Node), +) -> NodeIndex +where N: Clone, E: Clone, Ty: EdgeType, Ix: IndexType, - D: DisplayNode, ->( - g: &mut Graph, - n: &N, - node_transform: impl FnOnce(&mut Node), -) -> NodeIndex { + Dn: DisplayNode, + De: DisplayEdge, +{ let idx = NodeIndex::new(g.g.node_count() + 1); let mut n = Node::new(n.clone()); @@ -46,19 +51,20 @@ pub fn add_node_custom< } /// Helper function which adds user's edge to the [`super::Graph`] instance. -pub fn add_edge< +pub fn add_edge( + g: &mut Graph, + start: NodeIndex, + end: NodeIndex, + e: &E, +) -> EdgeIndex +where N: Clone, E: Clone, Ty: EdgeType, Ix: IndexType, Dn: DisplayNode, De: DisplayEdge, ->( - g: &mut Graph, - start: NodeIndex, - end: NodeIndex, - e: &E, -) -> EdgeIndex { +{ add_edge_custom( g, start, @@ -69,20 +75,21 @@ pub fn add_edge< } /// Helper function which adds user's edge to the [`super::Graph`] instance with custom edge transform function. -pub fn add_edge_custom< +pub fn add_edge_custom( + g: &mut Graph, + start: NodeIndex, + end: NodeIndex, + e: &E, + edge_transform: impl FnOnce(&mut Edge), +) -> EdgeIndex +where N: Clone, E: Clone, Ty: EdgeType, Ix: IndexType, Dn: DisplayNode, De: DisplayEdge, ->( - g: &mut Graph, - start: NodeIndex, - end: NodeIndex, - e: &E, - edge_transform: impl FnOnce(&mut Edge), -) -> EdgeIndex { +{ let mut edge = Edge::new(e.clone()); edge.set_id(EdgeIndex::::new(g.g.edge_count() + 1)); @@ -93,19 +100,11 @@ pub fn add_edge_custom< g.g.add_edge(start, end, edge) } -/// Helper function which transforms users [`petgraph::stable_graph::StableGraph`] isntance into the version required by the [`super::GraphView`] widget. -/// -/// The function creates a new `StableGraph` where the nodes and edges are encapsulated into -/// Node and Edge structs respectively. New nodes and edges are created with [`default_node_transform`] and [`default_edge_transform`] -/// functions. If you want to define custom transformation procedures (e.g. to use custom label for nodes), use [`to_graph_custom`] instead. -/// -/// # Arguments -/// * `g` - A reference to a [`petgraph::stable_graph::StableGraph`]. The graph can have any data type for nodes and edges, and -/// can be either directed or undirected. +/// Helper function which transforms [`petgraph::stable_graph::StableGraph`] into the [`super::Graph`] required by the [`super::GraphView`] widget. /// -/// # Returns -/// * A new [`petgraph::stable_graph::StableGraph`] with the same topology as the input graph, but the nodes and edges encapsulated -/// into Node and Edge structs compatible as an input to [`super::GraphView`] widget. +/// The function creates a new `StableGraph` where nodes and edges are represented by [`super::Node`] and [`super::Edge`] respectively. +/// New nodes and edges are created with [`default_node_transform`] and [`default_edge_transform`] functions. +/// If you want to define custom transformation procedures (e.g. to use custom label for nodes), use [`to_graph_custom`] instead. /// /// # Example /// ``` @@ -113,58 +112,53 @@ pub fn add_edge_custom< /// use egui_graphs::{to_graph, DefaultNodeShape, DefaultEdgeShape, Graph}; /// use egui::Pos2; /// -/// let mut user_graph: StableGraph<&str, &str> = StableGraph::new(); -/// let node1 = user_graph.add_node("A"); -/// let node2 = user_graph.add_node("B"); -/// user_graph.add_edge(node1, node2, "edge1"); +/// let mut g: StableGraph<&str, &str> = StableGraph::new(); +/// let node1 = g.add_node("A"); +/// let node2 = g.add_node("B"); +/// g.add_edge(node1, node2, "edge1"); /// -/// let input_graph: Graph<_, _, _, _, DefaultNodeShape, DefaultEdgeShape> = to_graph(&user_graph); +/// let result: Graph<_, _, _, _, DefaultNodeShape, DefaultEdgeShape> = to_graph(&g); /// -/// assert_eq!(input_graph.g.node_count(), 2); -/// assert_eq!(input_graph.g.edge_count(), 1); +/// assert_eq!(result.g.node_count(), 2); +/// assert_eq!(result.g.edge_count(), 1); /// -/// let mut input_indices = input_graph.g.node_indices(); -/// let input_node_1 = input_indices.next().unwrap(); -/// let input_node_2 = input_indices.next().unwrap(); -/// assert_eq!(*input_graph.g.node_weight(input_node_1).unwrap().payload(), "A"); -/// assert_eq!(*input_graph.g.node_weight(input_node_2).unwrap().payload(), "B"); +/// let mut indxs = result.g.node_indices(); +/// let result_node1 = indxs.next().unwrap(); +/// let result_node2 = indxs.next().unwrap(); +/// assert_eq!(*result.g.node_weight(result_node1).unwrap().payload(), "A"); +/// assert_eq!(*result.g.node_weight(result_node2).unwrap().payload(), "B"); /// -/// assert_eq!(*input_graph.g.edge_weight(input_graph.g.edge_indices().next().unwrap()).unwrap().payload(), "edge1"); +/// assert_eq!(*result.g.edge_weight(result.g.edge_indices().next().unwrap()).unwrap().payload(), "edge1"); /// -/// assert_eq!(*input_graph.g.node_weight(input_node_1).unwrap().label().clone(), format!("node {}", input_node_1.index())); -/// assert_eq!(*input_graph.g.node_weight(input_node_2).unwrap().label().clone(), format!("node {}", input_node_2.index())); -/// -/// let loc_1 = input_graph.g.node_weight(input_node_1).unwrap().location(); -/// let loc_2 = input_graph.g.node_weight(input_node_2).unwrap().location(); -/// assert!(loc_1 != Pos2::ZERO); -/// assert!(loc_2 != Pos2::ZERO); +/// assert_eq!(*result.g.node_weight(result_node1).unwrap().label().clone(), format!("node {}", result_node1.index())); +/// assert_eq!(*result.g.node_weight(result_node2).unwrap().label().clone(), format!("node {}", result_node2.index())); /// ``` -pub fn to_graph< +pub fn to_graph(g: &StableGraph) -> Graph +where N: Clone, E: Clone, Ty: EdgeType, Ix: IndexType, Dn: DisplayNode, De: DisplayEdge, ->( - g: &StableGraph, -) -> Graph { +{ transform(g, &mut default_node_transform, &mut default_edge_transform) } /// The same as [`to_graph`], but allows to define custom transformation procedures for nodes and edges. -pub fn to_graph_custom< +pub fn to_graph_custom( + g: &StableGraph, + mut node_transform: impl FnMut(&mut Node), + mut edge_transform: impl FnMut(&mut Edge), +) -> Graph +where N: Clone, E: Clone, Ty: EdgeType, Ix: IndexType, Dn: DisplayNode, De: DisplayEdge, ->( - g: &StableGraph, - mut node_transform: impl FnMut(&mut Node), - mut edge_transform: impl FnMut(&mut Edge), -) -> Graph { +{ transform(g, &mut node_transform, &mut edge_transform) } @@ -180,7 +174,6 @@ pub fn default_node_transform< node: &mut Node, ) { node.set_label(format!("node {}", node.id().index())); - node.set_location(random_location(DEFAULT_SPAWN_SIZE)); } /// Default edge transform function. Keeps original data and creates a new edge. @@ -197,23 +190,19 @@ pub fn default_edge_transform< edge.set_label(format!("edge {}", edge.id().index())); } -fn random_location(size: f32) -> Pos2 { - let mut rng = rand::thread_rng(); - Pos2::new(rng.gen_range(0. ..size), rng.gen_range(0. ..size)) -} - -fn transform< +fn transform( + input: &StableGraph, + node_transform: &mut impl FnMut(&mut Node), + edge_transform: &mut impl FnMut(&mut Edge), +) -> Graph +where N: Clone, E: Clone, Ty: EdgeType, Ix: IndexType, Dn: DisplayNode, De: DisplayEdge, ->( - input: &StableGraph, - node_transform: &mut impl FnMut(&mut Node), - edge_transform: &mut impl FnMut(&mut Edge), -) -> Graph { +{ let mut g = StableGraph::, Edge, Ty, Ix>::default(); @@ -252,6 +241,34 @@ fn transform< Graph::new(g) } +pub fn node_size>( + node: &Node, + dir: Vec2, +) -> f32 { + let connector_left = node.display().closest_boundary_point(dir); + let connector_right = node.display().closest_boundary_point(-dir); + + ((connector_right.to_vec2() - connector_left.to_vec2()) / 2.).length() +} + +pub fn random_graph(num_nodes: usize, num_edges: usize) -> Graph { + let mut rng = rand::thread_rng(); + let mut graph = StableGraph::new(); + + for _ in 0..num_nodes { + graph.add_node(()); + } + + for _ in 0..num_edges { + let source = rng.gen_range(0..num_nodes); + let target = rng.gen_range(0..num_nodes); + + graph.add_edge(NodeIndex::new(source), NodeIndex::new(target), ()); + } + + to_graph(&graph) +} + #[cfg(test)] mod tests { use crate::DefaultEdgeShape; @@ -279,10 +296,6 @@ mod tests { let input_n = input_g.g.node_weight(input_idx).unwrap(); assert_eq!(*input_n.payload(), *user_n); - - assert!(input_n.location().x >= 0.0 && input_n.location().x <= DEFAULT_SPAWN_SIZE); - assert!(input_n.location().y >= 0.0 && input_n.location().y <= DEFAULT_SPAWN_SIZE); - assert_eq!(*input_n.label(), format!("node {}", user_idx.index())); assert!(!input_n.selected()); @@ -308,10 +321,6 @@ mod tests { let input_n = input_g.g.node_weight(input_idx).unwrap(); assert_eq!(*input_n.payload(), *user_n); - - assert!(input_n.location().x >= 0.0 && input_n.location().x <= DEFAULT_SPAWN_SIZE); - assert!(input_n.location().y >= 0.0 && input_n.location().y <= DEFAULT_SPAWN_SIZE); - assert_eq!(*input_n.label(), format!("node {}", user_idx.index())); assert!(!input_n.selected()); diff --git a/src/layouts/hierarchical/layout.rs b/src/layouts/hierarchical/layout.rs new file mode 100644 index 0000000..e380895 --- /dev/null +++ b/src/layouts/hierarchical/layout.rs @@ -0,0 +1,113 @@ +use std::collections::HashSet; + +use egui::Pos2; +use petgraph::{ + csr::IndexType, + stable_graph::NodeIndex, + Direction::{Incoming, Outgoing}, + EdgeType, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + layouts::{Layout, LayoutState}, + DisplayEdge, DisplayNode, Graph, +}; + +const ROW_DIST: usize = 50; +const NODE_DIST: usize = 50; + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct State { + triggered: bool, +} + +impl LayoutState for State {} + +#[derive(Debug, Default)] +pub struct Hierarchical { + state: State, +} + +impl Layout for Hierarchical { + fn next(&mut self, g: &mut Graph) + where + N: Clone, + E: Clone, + Ty: EdgeType, + Ix: IndexType, + Dn: DisplayNode, + De: DisplayEdge, + { + if self.state.triggered { + return; + } + + let mut visited = HashSet::new(); + let mut max_col = 0; + g.g.externals(Incoming) + .collect::>>() + .iter() + .enumerate() + .for_each(|(i, root_idx)| { + visited.insert(*root_idx); + + let curr_max_col = build_tree(g, &mut visited, root_idx, 0, i); + if curr_max_col > max_col { + max_col = curr_max_col; + }; + }); + + self.state.triggered = true; + } + + fn state(&self) -> State { + self.state.clone() + } + + fn from_state(state: State) -> impl Layout { + Hierarchical { state } + } +} + +fn build_tree( + g: &mut Graph, + visited: &mut HashSet>, + root_idx: &NodeIndex, + start_row: usize, + start_col: usize, +) -> usize +where + N: Clone, + E: Clone, + Ty: EdgeType, + Ix: IndexType, + Dn: DisplayNode, + De: DisplayEdge, +{ + let y = start_row * ROW_DIST; + let x = start_col * NODE_DIST; + + let node = &mut g.g[*root_idx]; + node.set_layout_location(Pos2::new(x as f32, y as f32)); + + let mut max_col = start_col; + g.g.neighbors_directed(*root_idx, Outgoing) + .collect::>>() + .iter() + .enumerate() + .for_each(|(i, neighbour_idx)| { + if visited.contains(neighbour_idx) { + return; + }; + + visited.insert(*neighbour_idx); + + let curr_max_col = build_tree(g, visited, neighbour_idx, start_row + 1, start_col + i); + if curr_max_col > max_col { + max_col = curr_max_col; + }; + }); + + max_col +} diff --git a/src/layouts/hierarchical/mod.rs b/src/layouts/hierarchical/mod.rs new file mode 100644 index 0000000..6b06c1f --- /dev/null +++ b/src/layouts/hierarchical/mod.rs @@ -0,0 +1,3 @@ +mod layout; + +pub use layout::{Hierarchical, State}; diff --git a/src/layouts/layout.rs b/src/layouts/layout.rs new file mode 100644 index 0000000..8ecf7cd --- /dev/null +++ b/src/layouts/layout.rs @@ -0,0 +1,27 @@ +use egui::util::id_type_map::SerializableAny; +use petgraph::{stable_graph::IndexType, EdgeType}; + +use crate::{DisplayEdge, DisplayNode, Graph}; + +pub trait LayoutState: SerializableAny + Default {} + +pub trait Layout: Default +where + S: LayoutState, +{ + /// Creates a new layout from the given state. State is loaded and saved on every frame. + fn from_state(state: S) -> impl Layout; + + /// Called on every frame. It should update the graph layout aka nodes locations. + fn next(&mut self, g: &mut Graph) + where + N: Clone, + E: Clone, + Ty: EdgeType, + Ix: IndexType, + Dn: DisplayNode, + De: DisplayEdge; + + /// Returns the current state of the layout. + fn state(&self) -> S; +} diff --git a/src/layouts/mod.rs b/src/layouts/mod.rs new file mode 100644 index 0000000..e0c45f8 --- /dev/null +++ b/src/layouts/mod.rs @@ -0,0 +1,5 @@ +pub mod hierarchical; +pub mod random; + +mod layout; +pub use layout::{Layout, LayoutState}; diff --git a/src/layouts/random/layout.rs b/src/layouts/random/layout.rs new file mode 100644 index 0000000..f1581e7 --- /dev/null +++ b/src/layouts/random/layout.rs @@ -0,0 +1,57 @@ +use egui::Pos2; +use petgraph::stable_graph::IndexType; +use rand::Rng; +use serde::{Deserialize, Serialize}; + +use crate::{ + layouts::{Layout, LayoutState}, + Graph, +}; +const SPAWN_SIZE: f32 = 250.; + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct State { + triggered: bool, +} + +impl LayoutState for State {} + +/// Randomly places nodes on the canvas. Does not override existing locations. Applies once. +#[derive(Debug, Default)] +pub struct Random { + state: State, +} + +impl Layout for Random { + fn next(&mut self, g: &mut Graph) + where + N: Clone, + E: Clone, + Ty: petgraph::EdgeType, + Ix: IndexType, + Dn: crate::DisplayNode, + De: crate::DisplayEdge, + { + if self.state.triggered { + return; + } + + let mut rng = rand::thread_rng(); + for node in g.g.node_weights_mut() { + node.set_layout_location(Pos2::new( + rng.gen_range(0. ..SPAWN_SIZE), + rng.gen_range(0. ..SPAWN_SIZE), + )); + } + + self.state.triggered = true; + } + + fn state(&self) -> State { + self.state.clone() + } + + fn from_state(state: State) -> impl Layout { + Self { state } + } +} diff --git a/src/layouts/random/mod.rs b/src/layouts/random/mod.rs new file mode 100644 index 0000000..8d596d8 --- /dev/null +++ b/src/layouts/random/mod.rs @@ -0,0 +1,3 @@ +mod layout; + +pub use layout::{Random, State}; diff --git a/src/lib.rs b/src/lib.rs index 3de48a8..45ef8e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,17 +3,22 @@ mod elements; mod graph; mod graph_view; mod helpers; +mod layouts; mod metadata; mod settings; pub use draw::{DefaultEdgeShape, DefaultNodeShape, DisplayEdge, DisplayNode, DrawContext}; pub use elements::{Edge, EdgeProps, Node, NodeProps}; pub use graph::Graph; -pub use graph_view::GraphView; +pub use graph_view::{DefaultGraphView, GraphView}; pub use helpers::{ add_edge, add_edge_custom, add_node, add_node_custom, default_edge_transform, - default_node_transform, to_graph, to_graph_custom, + default_node_transform, node_size, random_graph, to_graph, to_graph_custom, }; +pub use layouts::hierarchical::{ + Hierarchical as LayoutHierarchical, State as LayoutStateHierarchical, +}; +pub use layouts::random::{Random as LayoutRandom, State as LayoutStateRandom}; pub use metadata::Metadata; pub use settings::{SettingsInteraction, SettingsNavigation, SettingsStyle}; diff --git a/src/metadata.rs b/src/metadata.rs index a62204d..361f165 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -1,13 +1,12 @@ use egui::{Id, Pos2, Rect, Vec2}; use petgraph::{stable_graph::IndexType, EdgeType}; +use serde::{Deserialize, Serialize}; -use crate::{DisplayNode, Node}; +use crate::{node_size, DisplayNode, Node}; -#[cfg_attr( - feature = "egui_persistence", - derive(serde::Serialize, serde::Deserialize) -)] -#[derive(Clone, Debug)] +const KEY: &str = "egui_graphs_metadata"; + +#[derive(Clone, Debug, Serialize, Deserialize)] struct Bounds { min: Vec2, max: Vec2, @@ -33,27 +32,24 @@ impl Bounds { &mut self, n: &Node, ) { + let size = node_size(n, Vec2::new(0., 1.)); let loc = n.location(); - if loc.x < self.min.x { - self.min.x = loc.x; + if loc.x + size < self.min.x { + self.min.x = loc.x + size; }; - if loc.x > self.max.x { - self.max.x = loc.x; + if loc.x + size > self.max.x { + self.max.x = loc.x + size; }; - if loc.y < self.min.y { - self.min.y = loc.y; + if loc.y - size < self.min.y { + self.min.y = loc.y - size; }; - if loc.y > self.max.y { - self.max.y = loc.y; + if loc.y + size > self.max.y { + self.max.y = loc.y + size; }; } } -#[cfg_attr( - feature = "egui_persistence", - derive(serde::Serialize, serde::Deserialize) -)] -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Metadata { /// Whether the frame is the first one pub first_frame: bool, @@ -81,13 +77,16 @@ impl Default for Metadata { } impl Metadata { - pub fn get(ui: &egui::Ui) -> Self { - ui.data_mut(|data| data.get_persisted::(Id::NULL).unwrap_or_default()) + pub fn load(ui: &egui::Ui) -> Self { + ui.data_mut(|data| { + data.get_persisted::(Id::new(KEY)) + .unwrap_or_default() + }) } - pub fn store_into_ui(self, ui: &mut egui::Ui) { + pub fn save(self, ui: &mut egui::Ui) { ui.data_mut(|data| { - data.insert_persisted(Id::NULL, self); + data.insert_persisted(Id::new(KEY), self); }); }