├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── README.md ├── animated_nodes │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── main.rs │ │ └── node.rs ├── basic │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── main.rs ├── basic_custom │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── main.rs ├── bevy_basic │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── main.rs ├── demo │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── drawers.rs │ │ ├── main.rs │ │ └── settings.rs ├── flex_nodes │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── main.rs │ │ └── node.rs ├── interactive │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── main.rs ├── label_change │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── main.rs ├── layouts │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── main.rs ├── multiple │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── main.rs ├── rainbow_edges │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── edge.rs │ │ └── main.rs ├── undirected │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── main.rs ├── wasm_custom_draw │ ├── Cargo.toml │ ├── README.md │ ├── Trunk.toml │ ├── assets │ │ ├── favicon.ico │ │ └── manifest.json │ ├── index.html │ └── src │ │ ├── main.rs │ │ └── node.rs └── window │ ├── Cargo.toml │ └── src │ └── main.rs └── src ├── draw ├── displays.rs ├── displays_default │ ├── edge.rs │ ├── edge_shape_builder.rs │ ├── mod.rs │ └── node.rs ├── drawer.rs └── mod.rs ├── elements ├── edge.rs ├── mod.rs └── node.rs ├── events ├── event.rs └── mod.rs ├── graph.rs ├── graph_view.rs ├── helpers.rs ├── layouts ├── hierarchical │ ├── layout.rs │ └── mod.rs ├── layout.rs ├── mod.rs └── random │ ├── layout.rs │ └── mod.rs ├── lib.rs ├── metadata.rs └── settings.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ci: 7 | name: ${{ matrix.os }} 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest, windows-latest, macos-latest] 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: cargo build 18 | run: cargo build --all-features 19 | 20 | - name: cargo fmt 21 | run: cargo fmt --all -- --check 22 | 23 | - name: cargo clippy 24 | run: cargo clippy -- -D warnings 25 | 26 | - name: cargo test 27 | run: cargo test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea 3 | /Cargo.lock 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "egui_graphs" 3 | version = "0.26.0" 4 | authors = ["Dmitrii Samsonov "] 5 | license = "MIT" 6 | homepage = "https://github.com/blitzarx1/egui_graphs" 7 | repository = "https://github.com/blitzarx1/egui_graphs" 8 | description = "Interactive graph visualization widget for rust powered by egui" 9 | edition = "2021" 10 | keywords = ["egui", "ui", "graph", "node-graph"] 11 | categories = ["gui", "visualization"] 12 | 13 | [dependencies] 14 | egui = { version = "0.31.0", default-features = false, features = [ 15 | "persistence", 16 | ] } 17 | rand = "0.9" 18 | petgraph = { version = "0.8", default-features = false, features = [ 19 | "graphmap", 20 | "stable_graph", 21 | "matrix_graph", 22 | "serde-1", 23 | ] } 24 | serde = { version = "1.0", features = ["derive"] } 25 | 26 | crossbeam = { version = "0.8", optional = true } 27 | 28 | [features] 29 | events = ["dep:crossbeam"] 30 | 31 | [workspace] 32 | members = ["examples/*"] 33 | 34 | [lints.rust] 35 | unsafe_code = "forbid" 36 | 37 | [lints.clippy] 38 | pedantic = { level = "deny", priority = 0 } 39 | enum_glob_use = { level = "deny", priority = 1 } 40 | perf = { level = "deny", priority = 2 } 41 | style = { level = "deny", priority = 3 } 42 | # unwrap_used = { level = "deny", priority = 4 } These should enabled in the future 43 | # expect_used = { level = "deny", priority = 5 } 44 | module_name_repetitions = { level = "allow", priority = 6 } 45 | cast_precision_loss = { level = "allow", priority = 7 } 46 | float_cmp = { level = "allow", priority = 8 } 47 | cast_possible_truncation = { level = "allow", priority = 9 } 48 | cast_sign_loss = { level = "allow", priority = 10 } 49 | out_of_bounds_indexing = { level = "allow", priority = 11 } 50 | 51 | must_use_candidate = { level = "allow", priority = 12 } 52 | struct_excessive_bools = { level = "allow", priority = 13 } 53 | return_self_not_must_use = { level = "allow", priority = 14 } 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![build](https://github.com/blitzarx1/egui_graphs/actions/workflows/rust.yml/badge.svg) 2 | [![Crates.io](https://img.shields.io/crates/v/egui_graphs)](https://crates.io/crates/egui_graphs) 3 | [![docs.rs](https://img.shields.io/docsrs/egui_graphs)](https://docs.rs/egui_graphs) 4 | 5 | # egui_graphs 6 | Graph visualization with rust, [petgraph](https://github.com/petgraph/petgraph) and [egui](https://github.com/emilk/egui) in its DNA. 7 | 8 | ![Screenshot 2023-04-28 at 23 14 38](https://user-images.githubusercontent.com/32969427/235233765-23b0673b-70e5-4138-9384-180804392dba.png) 9 | 10 | The project implements a Widget for the egui framework, enabling easy visualization of interactive graphs in rust. The goal is to implement the very basic engine for graph visualization within egui, which can be easily extended and customized for your needs. 11 | 12 | - [x] Visualization of any complex graphs; 13 | - [x] Zooming and panning; 14 | - [x] Node and Edge labels; 15 | - [x] Node and edges interactions and events reporting: click, double click, select, drag; 16 | - [x] Style configuration via egui context styles; 17 | - [x] Dark/Light theme support via egui context styles; 18 | - [x] Events reporting to extend the graph functionality by the user handling them; 19 | - [x] Layots and custom layout mechanism; 20 | 21 | ## Status 22 | The project is on track for a stable release v1.0.0. For the moment, breaking releases are very possible. 23 | 24 | Please use master branch for the latest updates. 25 | 26 | Check the [demo example](https://github.com/blitzarx1/egui_graphs/tree/master/examples/demo) for the comprehensive overview of the widget possibilities. 27 | 28 | ## Layouts 29 | 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: 30 | - [x] Random layout; 31 | - [x] Hierarchical layout; 32 | - [ ] Force-directed layout; (coming soon) 33 | 34 | ![Screenshot 2024-10-28 at 3 57 05 PM](https://github.com/user-attachments/assets/48614f43-4436-42eb-a238-af196d2044b4) 35 | 36 | Check the [layouts example](https://github.com/blitzarx1/egui_graphs/blob/master/examples/layouts/src/main.rs). 37 | 38 | ## Examples 39 | ### Basic setup example 40 | The source code of the following steps can be found in the [basic example](https://github.com/blitzarx1/egui_graphs/blob/master/examples/basic/src/main.rs). 41 | #### Step 1: Setting up the `BasicApp` struct. 42 | First, let's define the `BasicApp` struct that will hold the graph. 43 | ```rust 44 | pub struct BasicApp { 45 | g: egui_graphs::Graph, 46 | } 47 | ``` 48 | 49 | #### Step 2: Implementing the `new()` function. 50 | Next, implement the `new()` function for the `BasicApp` struct. 51 | ```rust 52 | impl BasicApp { 53 | fn new(_: &eframe::CreationContext<'_>) -> Self { 54 | let g = generate_graph(); 55 | Self { g: egui_graphs::Graph::from(&g) } 56 | } 57 | } 58 | ``` 59 | 60 | #### Step 3: Generating the graph. 61 | Create a helper function called `generate_graph()`. In this example, we create three nodes and three edges. 62 | ```rust 63 | fn generate_graph() -> petgraph::StableGraph<(), ()> { 64 | let mut g = petgraph::StableGraph::new(); 65 | 66 | let a = g.add_node(()); 67 | let b = g.add_node(()); 68 | let c = g.add_node(()); 69 | 70 | g.add_edge(a, b, ()); 71 | g.add_edge(b, c, ()); 72 | g.add_edge(c, a, ()); 73 | 74 | g 75 | } 76 | ``` 77 | 78 | #### Step 4: Implementing the `eframe::App` trait. 79 | Now, lets implement the `eframe::App` trait for the `BasicApp`. In the `update()` function, we create a `egui::CentralPanel` and add the `egui_graphs::GraphView` widget to it. 80 | ```rust 81 | impl eframe::App for BasicApp { 82 | fn update(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) { 83 | egui::CentralPanel::default().show(ctx, |ui| { 84 | ui.add(&mut egui_graphs::GraphView::new(&mut self.g)); 85 | }); 86 | } 87 | } 88 | ``` 89 | 90 | #### Step 5: Running the application. 91 | Finally, run the application using the `eframe::run_native()` function. 92 | ```rust 93 | fn main() { 94 | eframe::run_native( 95 | "egui_graphs_basic_demo", 96 | eframe::NativeOptions::default(), 97 | Box::new(|cc| Ok(Box::new(BasicApp::new(cc)))), 98 | ) 99 | .unwrap(); 100 | } 101 | ``` 102 | 103 | ![Screenshot 2023-10-14 at 23 49 49](https://github.com/blitzarx1/egui_graphs/assets/32969427/584b78de-bca3-421b-b003-9321fd3e1b13) 104 | You can further customize the appearance and behavior of your graph by modifying the settings or adding more nodes and edges as needed. 105 | 106 | ## Features 107 | ### Events 108 | Can be enabled with `events` feature. Events describe a change made in graph whether it changed zoom level or node dragging. 109 | 110 | Combining this feature with custom node draw function allows to implement custom node behavior and drawing according to the events happening. 111 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | This directory contains examples of how to use the egui_graphs library. 3 | 4 | ## Prerequisites 5 | All examples require rust to be installed. Just follow easy one step install from the official site https://www.rust-lang.org/tools/install. 6 | 7 | ## Run 8 | To run an example, use the following command: 9 | ```bash 10 | cargo run --release -p 11 | ``` 12 | where `` is the name of the example you want to run. 13 | -------------------------------------------------------------------------------- /examples/animated_nodes/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "animated_nodes" 3 | version = "0.1.0" 4 | authors = ["Dmitrii Samsonov "] 5 | license = "MIT" 6 | edition = "2021" 7 | 8 | [dependencies] 9 | egui_graphs = { path = "../.." } 10 | egui = "0.31" 11 | eframe = "0.31" 12 | petgraph = "0.8" 13 | -------------------------------------------------------------------------------- /examples/animated_nodes/README.md: -------------------------------------------------------------------------------- 1 | # Animated nodes 2 | 3 | Example demonstrates how to use custom drawing functions nodes, handle animation state and access `node` payload in the drawing function. 4 | 5 | Nodes are drawn as squares with labels in their center. Node is rotating when it is in the state of dragging. It is rotating clockwise if the `node` payload has `clockwise` field setting set to `true` and vice-verse. 6 | 7 | ## run 8 | 9 | ```bash 10 | cargo run --release -p animated_nodes 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/animated_nodes/src/main.rs: -------------------------------------------------------------------------------- 1 | use eframe::{run_native, App, CreationContext}; 2 | use egui::Context; 3 | use egui_graphs::{ 4 | default_edge_transform, default_node_transform, to_graph_custom, DefaultEdgeShape, Graph, 5 | GraphView, SettingsInteraction, SettingsNavigation, 6 | }; 7 | use node::NodeShapeAnimated; 8 | use petgraph::{ 9 | stable_graph::{DefaultIx, StableGraph}, 10 | Directed, 11 | }; 12 | 13 | mod node; 14 | 15 | const GLYPH_CLOCKWISE: &str = "↻"; 16 | const GLYPH_ANTICLOCKWISE: &str = "↺"; 17 | 18 | pub struct AnimatedNodesApp { 19 | g: Graph, 20 | } 21 | 22 | impl AnimatedNodesApp { 23 | fn new(_: &CreationContext<'_>) -> Self { 24 | let g = generate_graph(); 25 | Self { 26 | g: to_graph_custom( 27 | &g, 28 | |n| { 29 | if n.payload().clockwise { 30 | default_node_transform(n); 31 | n.set_label(GLYPH_CLOCKWISE.to_string()); 32 | } else { 33 | default_node_transform(n); 34 | n.set_label(GLYPH_ANTICLOCKWISE.to_string()); 35 | } 36 | }, 37 | default_edge_transform, 38 | ), 39 | } 40 | } 41 | } 42 | 43 | impl App for AnimatedNodesApp { 44 | fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { 45 | egui::CentralPanel::default().show(ctx, |ui| { 46 | ui.add( 47 | &mut GraphView::<_, _, _, _, NodeShapeAnimated, DefaultEdgeShape>::new(&mut self.g) 48 | .with_navigations( 49 | &SettingsNavigation::default() 50 | .with_fit_to_screen_enabled(false) 51 | .with_zoom_and_pan_enabled(true), 52 | ) 53 | .with_interactions( 54 | &SettingsInteraction::default() 55 | .with_dragging_enabled(true) 56 | .with_node_selection_enabled(true) 57 | .with_edge_selection_enabled(true), 58 | ), 59 | ); 60 | }); 61 | } 62 | } 63 | 64 | fn generate_graph() -> StableGraph { 65 | let mut g = StableGraph::new(); 66 | 67 | let a = g.add_node(node::NodeData { clockwise: true }); 68 | let b = g.add_node(node::NodeData { clockwise: false }); 69 | let c = g.add_node(node::NodeData { clockwise: false }); 70 | 71 | g.add_edge(a, b, ()); 72 | g.add_edge(b, c, ()); 73 | g.add_edge(c, a, ()); 74 | 75 | g 76 | } 77 | 78 | fn main() { 79 | let native_options = eframe::NativeOptions::default(); 80 | run_native( 81 | "egui_graphs_animated_nodes_demo", 82 | native_options, 83 | Box::new(|cc| Ok(Box::new(AnimatedNodesApp::new(cc)))), 84 | ) 85 | .unwrap(); 86 | } 87 | -------------------------------------------------------------------------------- /examples/animated_nodes/src/node.rs: -------------------------------------------------------------------------------- 1 | use std::time::Instant; 2 | 3 | use egui::{ 4 | emath::Rot2, epaint::TextShape, Color32, FontFamily, FontId, Pos2, Rect, Shape, Stroke, Vec2, 5 | }; 6 | use egui_graphs::{DisplayNode, NodeProps}; 7 | use petgraph::{stable_graph::IndexType, EdgeType}; 8 | 9 | pub trait IsClockwise { 10 | fn get_is_clockwise(&self) -> bool; 11 | } 12 | 13 | #[derive(Clone, Debug)] 14 | pub struct NodeData { 15 | pub clockwise: bool, 16 | } 17 | 18 | impl IsClockwise for NodeData { 19 | fn get_is_clockwise(&self) -> bool { 20 | self.clockwise 21 | } 22 | } 23 | 24 | /// Rotates node when the node is being dragged. 25 | #[derive(Clone)] 26 | pub struct NodeShapeAnimated { 27 | label: String, 28 | loc: Pos2, 29 | dragged: bool, 30 | clockwise: bool, 31 | 32 | angle_rad: f32, 33 | speed_per_second: f32, 34 | /// None means animation is not in progress 35 | last_time_update: Option, 36 | 37 | size: f32, 38 | } 39 | 40 | impl NodeShapeAnimated { 41 | pub fn get_rotation_increment(&mut self) -> f32 { 42 | let now = Instant::now(); 43 | let mult = match self.clockwise { 44 | true => 1., 45 | false => -1., 46 | }; 47 | match self.last_time_update { 48 | Some(last_time) => { 49 | self.last_time_update = Some(now); 50 | let seconds_passed = now.duration_since(last_time); 51 | seconds_passed.as_secs_f32() * self.speed_per_second * mult 52 | } 53 | None => { 54 | self.last_time_update = Some(now); 55 | 0. 56 | } 57 | } 58 | } 59 | } 60 | 61 | impl From> for NodeShapeAnimated { 62 | fn from(node_props: NodeProps) -> Self { 63 | Self { 64 | label: node_props.label.clone(), 65 | loc: node_props.location(), 66 | dragged: node_props.dragged, 67 | clockwise: node_props.payload.get_is_clockwise(), 68 | 69 | angle_rad: Default::default(), 70 | last_time_update: Default::default(), 71 | speed_per_second: 1., 72 | 73 | size: 30., 74 | } 75 | } 76 | } 77 | 78 | impl DisplayNode 79 | for NodeShapeAnimated 80 | { 81 | fn is_inside(&self, pos: Pos2) -> bool { 82 | let rotated_pos = rotate_point_around(self.loc, pos, -self.angle_rad); 83 | let rect = Rect::from_center_size(self.loc, Vec2::new(self.size, self.size)); 84 | 85 | rect.contains(rotated_pos) 86 | } 87 | 88 | fn closest_boundary_point(&self, dir: Vec2) -> Pos2 { 89 | let rotated_dir = rotate_vector(dir, -self.angle_rad); 90 | let intersection_point = find_intersection(self.loc, self.size, rotated_dir); 91 | rotate_point_around(self.loc, intersection_point, self.angle_rad) 92 | } 93 | 94 | fn shapes(&mut self, ctx: &egui_graphs::DrawContext) -> Vec { 95 | // lets draw a rect with label in the center for every node 96 | // which rotates when the node is dragged 97 | 98 | // find node center location on the screen coordinates 99 | let center = ctx.meta.canvas_to_screen_pos(self.loc); 100 | let size = ctx.meta.canvas_to_screen_size(self.size); 101 | let rect_default = Rect::from_center_size(center, Vec2::new(size, size)); 102 | let color = ctx.ctx.style().visuals.weak_text_color(); 103 | 104 | let diff = match self.dragged { 105 | true => self.get_rotation_increment(), 106 | false => { 107 | if self.last_time_update.is_some() { 108 | self.last_time_update = None; 109 | } 110 | 0. 111 | } 112 | }; 113 | 114 | if diff.abs() > 0. { 115 | let curr_angle = self.angle_rad + diff; 116 | let rot = Rot2::from_angle(curr_angle).normalized(); 117 | self.angle_rad = rot.angle(); 118 | }; 119 | 120 | let points = rect_to_points(rect_default) 121 | .into_iter() 122 | .map(|p| rotate_point_around(center, p, self.angle_rad)) 123 | .collect::>(); 124 | 125 | let shape_rect = Shape::convex_polygon(points, Color32::default(), Stroke::new(1., color)); 126 | 127 | // create label 128 | let color = ctx.ctx.style().visuals.text_color(); 129 | let galley = ctx.ctx.fonts(|f| { 130 | f.layout_no_wrap( 131 | self.label.clone(), 132 | FontId::new(ctx.meta.canvas_to_screen_size(10.), FontFamily::Monospace), 133 | color, 134 | ) 135 | }); 136 | 137 | // we need to offset label by half its size to place it in the center of the rect 138 | let offset = Vec2::new(-galley.size().x / 2., -galley.size().y / 2.); 139 | 140 | // create the shape and add it to the layers 141 | let shape_label = TextShape::new(center + offset, galley, color); 142 | 143 | vec![shape_rect, shape_label.into()] 144 | } 145 | 146 | fn update(&mut self, state: &NodeProps) { 147 | self.label = state.label.clone(); 148 | self.loc = state.location(); 149 | self.dragged = state.dragged; 150 | self.clockwise = state.payload.get_is_clockwise(); 151 | } 152 | } 153 | 154 | fn find_intersection(center: Pos2, size: f32, direction: Vec2) -> Pos2 { 155 | if direction.x.abs() > direction.y.abs() { 156 | // intersects left or right side 157 | let x = if direction.x > 0.0 { 158 | center.x + size / 2.0 159 | } else { 160 | center.x - size / 2.0 161 | }; 162 | let y = center.y + direction.y / direction.x * (x - center.x); 163 | Pos2::new(x, y) 164 | } else { 165 | // intersects top or bottom side 166 | let y = if direction.y > 0.0 { 167 | center.y + size / 2.0 168 | } else { 169 | center.y - size / 2.0 170 | }; 171 | let x = center.x + direction.x / direction.y * (y - center.y); 172 | Pos2::new(x, y) 173 | } 174 | } 175 | 176 | // Function to rotate a point around another point 177 | fn rotate_point_around(center: Pos2, point: Pos2, angle: f32) -> Pos2 { 178 | let sin_angle = angle.sin(); 179 | let cos_angle = angle.cos(); 180 | 181 | // translate point back to origin 182 | let translated_point = point - center; 183 | 184 | // rotate point 185 | let rotated_x = translated_point.x * cos_angle - translated_point.y * sin_angle; 186 | let rotated_y = translated_point.x * sin_angle + translated_point.y * cos_angle; 187 | 188 | // translate point back 189 | Pos2::new(rotated_x, rotated_y) + center.to_vec2() 190 | } 191 | 192 | fn rect_to_points(rect: Rect) -> Vec { 193 | let top_left = rect.min; 194 | let bottom_right = rect.max; 195 | 196 | // calculate the other two corners 197 | let top_right = Pos2::new(bottom_right.x, top_left.y); 198 | let bottom_left = Pos2::new(top_left.x, bottom_right.y); 199 | 200 | vec![top_left, top_right, bottom_right, bottom_left] 201 | } 202 | 203 | /// rotates vector by angle 204 | fn rotate_vector(vec: Vec2, angle: f32) -> Vec2 { 205 | let cos = angle.cos(); 206 | let sin = angle.sin(); 207 | Vec2::new(cos * vec.x - sin * vec.y, sin * vec.x + cos * vec.y) 208 | } 209 | 210 | #[cfg(test)] 211 | mod tests { 212 | use super::*; 213 | 214 | #[test] 215 | fn test_intersection_right_side() { 216 | let center = Pos2::new(0.0, 0.0); 217 | let size = 10.; 218 | let direction = Vec2::new(1.0, 0.0); 219 | let expected = Pos2::new(5.0, 0.0); 220 | assert_eq!(find_intersection(center, size, direction), expected); 221 | } 222 | 223 | #[test] 224 | fn test_intersection_top_side() { 225 | let center = Pos2::new(0.0, 0.0); 226 | let size = 10.; 227 | let direction = Vec2::new(0.0, 1.0); 228 | let expected = Pos2::new(0.0, 5.0); 229 | assert_eq!(find_intersection(center, size, direction), expected); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /examples/basic/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "basic" 3 | version = "0.1.0" 4 | authors = ["Dmitrii Samsonov "] 5 | license = "MIT" 6 | edition = "2021" 7 | 8 | [dependencies] 9 | egui_graphs = { path = "../.." } 10 | egui = "0.31" 11 | eframe = "0.31" 12 | petgraph = "0.8" 13 | -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | # Basic 2 | 3 | Basic example which demonstrates the usage of `GraphView` widget. 4 | 5 | ## run 6 | 7 | ```bash 8 | cargo run --release -p basic 9 | ``` 10 | -------------------------------------------------------------------------------- /examples/basic/src/main.rs: -------------------------------------------------------------------------------- 1 | use eframe::{run_native, App, CreationContext, NativeOptions}; 2 | use egui::Context; 3 | use egui_graphs::{DefaultGraphView, Graph}; 4 | use petgraph::stable_graph::StableGraph; 5 | 6 | pub struct BasicApp { 7 | g: Graph, 8 | } 9 | 10 | impl BasicApp { 11 | fn new(_: &CreationContext<'_>) -> Self { 12 | let g = generate_graph(); 13 | Self { g: Graph::from(&g) } 14 | } 15 | } 16 | 17 | impl App for BasicApp { 18 | fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { 19 | egui::CentralPanel::default().show(ctx, |ui| { 20 | ui.add(&mut DefaultGraphView::new(&mut self.g)); 21 | }); 22 | } 23 | } 24 | 25 | fn generate_graph() -> StableGraph<(), ()> { 26 | let mut g = StableGraph::new(); 27 | 28 | let a = g.add_node(()); 29 | let b = g.add_node(()); 30 | let c = g.add_node(()); 31 | 32 | g.add_edge(a, b, ()); 33 | g.add_edge(b, c, ()); 34 | g.add_edge(c, a, ()); 35 | 36 | g 37 | } 38 | 39 | fn main() { 40 | run_native( 41 | "egui_graphs_basic_demo", 42 | NativeOptions::default(), 43 | Box::new(|cc| Ok(Box::new(BasicApp::new(cc)))), 44 | ) 45 | .unwrap(); 46 | } 47 | -------------------------------------------------------------------------------- /examples/basic_custom/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "basic_custom" 3 | version = "0.1.0" 4 | authors = ["Dmitrii Samsonov "] 5 | license = "MIT" 6 | edition = "2021" 7 | 8 | [dependencies] 9 | egui_graphs = { path = "../.." } 10 | egui = "0.31" 11 | eframe = "0.31" 12 | petgraph = "0.8" -------------------------------------------------------------------------------- /examples/basic_custom/README.md: -------------------------------------------------------------------------------- 1 | # Basic 2 | 3 | Basic example which demonstrates the usage of `GraphView` widget with helper functions. 4 | 5 | ## run 6 | 7 | ```bash 8 | cargo run --release -p basic_custom 9 | ``` 10 | -------------------------------------------------------------------------------- /examples/basic_custom/src/main.rs: -------------------------------------------------------------------------------- 1 | use eframe::{run_native, App, CreationContext, NativeOptions}; 2 | use egui::{Context, Pos2}; 3 | use egui_graphs::{DefaultGraphView, Graph, SettingsStyle}; 4 | use petgraph::stable_graph::StableGraph; 5 | 6 | pub struct BasicCustomApp { 7 | g: Graph, 8 | } 9 | 10 | impl BasicCustomApp { 11 | fn new(_: &CreationContext<'_>) -> Self { 12 | let mut g = Graph::new(StableGraph::default()); 13 | 14 | let positions = vec![Pos2::new(0., 0.), Pos2::new(50., 0.), Pos2::new(0., 50.)]; 15 | let mut idxs = Vec::with_capacity(positions.len()); 16 | for position in positions { 17 | let idx = g.add_node_with_label_and_location((), position.to_string(), position); 18 | 19 | idxs.push(idx); 20 | } 21 | 22 | g.add_edge(idxs[0], idxs[1], ()); 23 | g.add_edge(idxs[1], idxs[2], ()); 24 | g.add_edge(idxs[2], idxs[0], ()); 25 | 26 | Self { g } 27 | } 28 | } 29 | 30 | impl App for BasicCustomApp { 31 | fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { 32 | egui::CentralPanel::default().show(ctx, |ui| { 33 | ui.add( 34 | &mut DefaultGraphView::new(&mut self.g) 35 | .with_styles(&SettingsStyle::default().with_labels_always(true)), 36 | ); 37 | }); 38 | } 39 | } 40 | 41 | fn main() { 42 | let native_options = NativeOptions::default(); 43 | run_native( 44 | "egui_graphs_basic_custom_demo", 45 | native_options, 46 | Box::new(|cc| Ok(Box::new(BasicCustomApp::new(cc)))), 47 | ) 48 | .unwrap(); 49 | } 50 | -------------------------------------------------------------------------------- /examples/bevy_basic/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_basic" 3 | version = "0.1.0" 4 | authors = ["Dmitrii Samsonov "] 5 | license = "MIT" 6 | edition = "2021" 7 | 8 | [dependencies] 9 | egui_graphs = { path = "../.." } 10 | bevy = "0.15" 11 | bevy_egui = "0.33" 12 | petgraph = "0.8" -------------------------------------------------------------------------------- /examples/bevy_basic/README.md: -------------------------------------------------------------------------------- 1 | # Bevy Basic 2 | Basic example which demonstrates the usage of `GraphView` widget in Bevy. 3 | 4 | ## run 5 | ```bash 6 | cargo run --release -p bevy_basic 7 | ``` 8 | -------------------------------------------------------------------------------- /examples/bevy_basic/src/main.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_egui::{egui, EguiContexts, EguiPlugin}; 3 | use egui_graphs::{Graph, GraphView, LayoutHierarchical, LayoutStateHierarchical}; 4 | use petgraph::stable_graph::StableGraph; 5 | 6 | fn main() { 7 | App::new() 8 | .add_plugins(DefaultPlugins) 9 | .add_plugins(EguiPlugin) 10 | .add_systems(Startup, setup) 11 | .add_systems(Update, update_graph) 12 | .run(); 13 | } 14 | 15 | #[derive(Component)] 16 | pub struct BasicGraph(pub Graph<(), ()>); 17 | 18 | impl BasicGraph { 19 | fn new() -> Self { 20 | let g = generate_graph(); 21 | Self(Graph::from(&g)) 22 | } 23 | } 24 | 25 | fn generate_graph() -> StableGraph<(), ()> { 26 | let mut g = StableGraph::new(); 27 | 28 | let a = g.add_node(()); 29 | let b = g.add_node(()); 30 | let c = g.add_node(()); 31 | 32 | g.add_edge(a, b, ()); 33 | g.add_edge(b, c, ()); 34 | g.add_edge(c, a, ()); 35 | 36 | g 37 | } 38 | 39 | fn setup(mut commands: Commands) { 40 | // add an entity with an egui_graphs::Graph component 41 | commands.spawn(BasicGraph::new()); 42 | } 43 | 44 | fn update_graph(mut contexts: EguiContexts, mut q_graph: Query<&mut BasicGraph>) { 45 | let ctx = contexts.ctx_mut(); 46 | let mut graph = q_graph.single_mut(); 47 | 48 | egui::CentralPanel::default().show(ctx, |ui| { 49 | ui.add(&mut GraphView::< 50 | _, 51 | _, 52 | _, 53 | _, 54 | _, 55 | _, 56 | LayoutStateHierarchical, 57 | LayoutHierarchical, 58 | >::new(&mut graph.0)); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /examples/demo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "demo" 3 | version = "0.1.0" 4 | authors = ["Dmitrii Samsonov "] 5 | license = "MIT" 6 | edition = "2021" 7 | 8 | [dependencies] 9 | egui_graphs = { version = "0.24", features = ["events"] } 10 | egui = "0.31" 11 | eframe = "0.31" 12 | serde_json = "1.0" 13 | petgraph = "0.7" 14 | # TODO: need to implement fdg to upgrade to newer petgraph and egui_graphs 15 | fdg = { git = "https://github.com/grantshandy/fdg" } 16 | rand = "0.9" 17 | crossbeam = "0.8" 18 | -------------------------------------------------------------------------------- /examples/demo/README.md: -------------------------------------------------------------------------------- 1 | # Demo 2 | 3 | Demo example allows to alter settings of the `GraphView` widget and see the results immediately. This example also demonstrates the usage of force-directed graph layout implemented on client side with interactive control as well. 4 | 5 | ## run 6 | 7 | ```bash 8 | cargo run --release -p demo 9 | ``` 10 | -------------------------------------------------------------------------------- /examples/demo/src/drawers.rs: -------------------------------------------------------------------------------- 1 | use egui::Ui; 2 | 3 | pub struct ValuesConfigButtonsStartReset { 4 | pub simulation_stopped: bool, 5 | } 6 | 7 | pub fn draw_start_reset_buttons( 8 | ui: &mut egui::Ui, 9 | mut values: ValuesConfigButtonsStartReset, 10 | mut on_change: impl FnMut(&mut egui::Ui, bool, bool), 11 | ) { 12 | ui.vertical(|ui| { 13 | ui.label("Stop or start simulation again or reset to default settings."); 14 | ui.horizontal(|ui| { 15 | let start_simulation_stopped = values.simulation_stopped; 16 | if ui 17 | .button(match values.simulation_stopped { 18 | true => "start", 19 | false => "stop", 20 | }) 21 | .clicked() 22 | { 23 | values.simulation_stopped = !values.simulation_stopped; 24 | }; 25 | 26 | let mut reset_pressed = false; 27 | if ui.button("reset").clicked() { 28 | reset_pressed = true; 29 | } 30 | 31 | if start_simulation_stopped != values.simulation_stopped || reset_pressed { 32 | on_change(ui, values.simulation_stopped, reset_pressed); 33 | } 34 | }); 35 | }); 36 | } 37 | 38 | pub struct ValuesSectionDebug { 39 | pub zoom: f32, 40 | pub pan: [f32; 2], 41 | pub fps: f32, 42 | } 43 | 44 | pub fn draw_section_debug(ui: &mut egui::Ui, values: ValuesSectionDebug) { 45 | ui.label(format!("zoom: {:.5}", values.zoom)); 46 | ui.label(format!("pan: [{:.5}, {:.5}]", values.pan[0], values.pan[1])); 47 | ui.label(format!("FPS: {:.1}", values.fps)); 48 | } 49 | 50 | pub struct ValuesConfigSlidersGraph { 51 | pub node_cnt: usize, 52 | pub edge_cnt: usize, 53 | } 54 | 55 | pub fn draw_counts_sliders( 56 | ui: &mut egui::Ui, 57 | mut values: ValuesConfigSlidersGraph, 58 | mut on_change: impl FnMut(i32, i32), 59 | ) { 60 | let start_node_cnt = values.node_cnt; 61 | let mut delta_node_cnt = 0; 62 | ui.horizontal(|ui| { 63 | if ui 64 | .add(egui::Slider::new(&mut values.node_cnt, 1..=2500).text("nodes")) 65 | .changed() 66 | { 67 | delta_node_cnt = values.node_cnt as i32 - start_node_cnt as i32; 68 | }; 69 | }); 70 | 71 | let start = values.edge_cnt; 72 | let mut delta_edge_cnt = 0; 73 | ui.horizontal(|ui| { 74 | if ui 75 | .add(egui::Slider::new(&mut values.edge_cnt, 1..=2500).text("edges")) 76 | .changed() 77 | { 78 | delta_edge_cnt = values.edge_cnt as i32 - start as i32; 79 | }; 80 | }); 81 | 82 | if delta_node_cnt != 0 || delta_edge_cnt != 0 { 83 | on_change(delta_node_cnt, delta_edge_cnt) 84 | }; 85 | } 86 | 87 | pub struct ValuesConfigSlidersSimulation { 88 | pub dt: f32, 89 | pub cooloff_factor: f32, 90 | pub scale: f32, 91 | } 92 | 93 | pub fn draw_simulation_config_sliders( 94 | ui: &mut Ui, 95 | mut values: ValuesConfigSlidersSimulation, 96 | mut on_change: impl FnMut(f32, f32, f32), 97 | ) { 98 | let start_dt = values.dt; 99 | let mut delta_dt = 0.; 100 | ui.horizontal(|ui| { 101 | if ui 102 | .add(egui::Slider::new(&mut values.dt, 0.00..=1.).text("dt")) 103 | .changed() 104 | { 105 | delta_dt = values.dt - start_dt; 106 | }; 107 | }); 108 | 109 | let start_cooloff_factor = values.cooloff_factor; 110 | let mut delta_cooloff_factor = 0.; 111 | ui.horizontal(|ui| { 112 | if ui 113 | .add(egui::Slider::new(&mut values.cooloff_factor, 0.00..=1.).text("cooloff_factor")) 114 | .changed() 115 | { 116 | delta_cooloff_factor = values.cooloff_factor - start_cooloff_factor; 117 | }; 118 | }); 119 | 120 | let start_scale = values.scale; 121 | let mut delta_scale = 0.; 122 | ui.horizontal(|ui| { 123 | if ui 124 | .add(egui::Slider::new(&mut values.scale, 1.0..=1000.).text("scale")) 125 | .changed() 126 | { 127 | delta_scale = values.scale - start_scale; 128 | }; 129 | }); 130 | 131 | if delta_dt != 0. || delta_cooloff_factor != 0. || delta_scale != 0. { 132 | on_change(delta_dt, delta_cooloff_factor, delta_scale); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /examples/demo/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Instant; 2 | 3 | use crossbeam::channel::{unbounded, Receiver, Sender}; 4 | use drawers::ValuesSectionDebug; 5 | use eframe::{run_native, App, CreationContext}; 6 | use egui::{CollapsingHeader, Context, Pos2, ScrollArea, Ui, Vec2}; 7 | use egui_graphs::events::Event; 8 | use egui_graphs::{random_graph, DefaultGraphView, Edge, Graph, Node}; 9 | use fdg::fruchterman_reingold::{FruchtermanReingold, FruchtermanReingoldConfiguration}; 10 | use fdg::nalgebra::{Const, OPoint}; 11 | use fdg::{Force, ForceGraph}; 12 | use petgraph::stable_graph::{DefaultIx, EdgeIndex, NodeIndex}; 13 | use petgraph::Directed; 14 | use rand::Rng; 15 | 16 | mod drawers; 17 | mod settings; 18 | 19 | const EVENTS_LIMIT: usize = 100; 20 | 21 | pub struct DemoApp { 22 | g: Graph<(), (), Directed, DefaultIx>, 23 | sim: ForceGraph, Edge<(), ()>>, 24 | force: FruchtermanReingold, 25 | 26 | settings_simulation: settings::SettingsSimulation, 27 | 28 | settings_graph: settings::SettingsGraph, 29 | settings_interaction: settings::SettingsInteraction, 30 | settings_navigation: settings::SettingsNavigation, 31 | settings_style: settings::SettingsStyle, 32 | 33 | last_events: Vec, 34 | 35 | simulation_stopped: bool, 36 | 37 | fps: f32, 38 | last_update_time: Instant, 39 | frames_last_time_span: usize, 40 | 41 | event_publisher: Sender, 42 | event_consumer: Receiver, 43 | 44 | pan: [f32; 2], 45 | zoom: f32, 46 | } 47 | 48 | impl DemoApp { 49 | fn new(_: &CreationContext<'_>) -> Self { 50 | let settings_graph = settings::SettingsGraph::default(); 51 | let settings_simulation = settings::SettingsSimulation::default(); 52 | 53 | let mut g = random_graph(settings_graph.count_node, settings_graph.count_edge); 54 | 55 | let mut force = init_force(&settings_simulation); 56 | let mut sim = fdg::init_force_graph_uniform(g.g.clone(), 1.0); 57 | force.apply(&mut sim); 58 | g.g.node_weights_mut().for_each(|node| { 59 | let point: fdg::nalgebra::OPoint> = 60 | sim.node_weight(node.id()).unwrap().1; 61 | node.set_location(Pos2::new(point.coords.x, point.coords.y)); 62 | }); 63 | 64 | let (event_publisher, event_consumer) = unbounded(); 65 | 66 | Self { 67 | g, 68 | sim, 69 | force, 70 | 71 | event_consumer, 72 | event_publisher, 73 | 74 | settings_graph, 75 | settings_simulation, 76 | 77 | settings_interaction: settings::SettingsInteraction::default(), 78 | settings_navigation: settings::SettingsNavigation::default(), 79 | settings_style: settings::SettingsStyle::default(), 80 | 81 | last_events: Vec::default(), 82 | 83 | simulation_stopped: false, 84 | 85 | fps: 0., 86 | last_update_time: Instant::now(), 87 | frames_last_time_span: 0, 88 | 89 | pan: [0., 0.], 90 | zoom: 0., 91 | } 92 | } 93 | 94 | /// applies forces if simulation is running 95 | fn update_simulation(&mut self) { 96 | if self.simulation_stopped { 97 | return; 98 | } 99 | 100 | self.force.apply(&mut self.sim); 101 | } 102 | 103 | /// sync locations computed by the simulation with egui_graphs::Graph nodes. 104 | fn sync(&mut self) { 105 | self.g.g.node_weights_mut().for_each(|node| { 106 | let sim_computed_point: OPoint> = 107 | self.sim.node_weight(node.id()).unwrap().1; 108 | node.set_location(Pos2::new( 109 | sim_computed_point.coords.x, 110 | sim_computed_point.coords.y, 111 | )); 112 | }); 113 | } 114 | 115 | fn update_fps(&mut self) { 116 | self.frames_last_time_span += 1; 117 | let now = Instant::now(); 118 | let elapsed = now.duration_since(self.last_update_time); 119 | if elapsed.as_secs() >= 1 { 120 | self.last_update_time = now; 121 | self.fps = self.frames_last_time_span as f32 / elapsed.as_secs_f32(); 122 | self.frames_last_time_span = 0; 123 | } 124 | } 125 | 126 | fn handle_events(&mut self) { 127 | self.event_consumer.try_iter().for_each(|e| { 128 | if self.last_events.len() > EVENTS_LIMIT { 129 | self.last_events.remove(0); 130 | } 131 | self.last_events.push(serde_json::to_string(&e).unwrap()); 132 | 133 | match e { 134 | Event::Pan(payload) => self.pan = payload.new_pan, 135 | Event::Zoom(payload) => self.zoom = payload.new_zoom, 136 | Event::NodeMove(payload) => { 137 | let node_id = NodeIndex::new(payload.id); 138 | 139 | self.sim.node_weight_mut(node_id).unwrap().1.coords.x = payload.new_pos[0]; 140 | self.sim.node_weight_mut(node_id).unwrap().1.coords.y = payload.new_pos[1]; 141 | } 142 | _ => {} 143 | } 144 | }); 145 | } 146 | 147 | fn random_node_idx(&self) -> Option { 148 | let nodes_cnt = self.g.node_count(); 149 | if nodes_cnt == 0 { 150 | return None; 151 | } 152 | 153 | let random_n_idx = rand::rng().random_range(0..nodes_cnt); 154 | self.g.g.node_indices().nth(random_n_idx) 155 | } 156 | 157 | fn random_edge_idx(&self) -> Option { 158 | let edges_cnt = self.g.edge_count(); 159 | if edges_cnt == 0 { 160 | return None; 161 | } 162 | 163 | let random_e_idx = rand::rng().random_range(0..edges_cnt); 164 | self.g.g.edge_indices().nth(random_e_idx) 165 | } 166 | 167 | fn remove_random_node(&mut self) { 168 | let idx = self.random_node_idx().unwrap(); 169 | self.remove_node(idx); 170 | } 171 | 172 | fn add_random_node(&mut self) { 173 | let random_n_idx = self.random_node_idx(); 174 | if random_n_idx.is_none() { 175 | return; 176 | } 177 | 178 | let random_n = self.g.node(random_n_idx.unwrap()).unwrap(); 179 | 180 | // location of new node is in in the closest surrounding of random existing node 181 | let mut rng = rand::rng(); 182 | let location = Pos2::new( 183 | random_n.location().x + 10. + rng.random_range(0. ..50.), 184 | random_n.location().y + 10. + rng.random_range(0. ..50.), 185 | ); 186 | 187 | let g_idx = self.g.add_node_with_location((), location); 188 | 189 | let sim_node = egui_graphs::Node::new(()); 190 | let sim_node_loc = fdg::nalgebra::Point2::new(location.x, location.y); 191 | 192 | let sim_idx = self.sim.add_node((sim_node, sim_node_loc)); 193 | 194 | assert_eq!(g_idx, sim_idx); 195 | } 196 | 197 | fn remove_node(&mut self, idx: NodeIndex) { 198 | self.g.remove_node(idx); 199 | 200 | self.sim.remove_node(idx).unwrap(); 201 | 202 | // update edges count 203 | self.settings_graph.count_edge = self.g.edge_count(); 204 | } 205 | 206 | fn add_random_edge(&mut self) { 207 | let random_start = self.random_node_idx().unwrap(); 208 | let random_end = self.random_node_idx().unwrap(); 209 | 210 | self.add_edge(random_start, random_end); 211 | } 212 | 213 | fn add_edge(&mut self, start: NodeIndex, end: NodeIndex) { 214 | self.g.add_edge(start, end, ()); 215 | 216 | self.sim.add_edge(start, end, egui_graphs::Edge::new(())); 217 | } 218 | 219 | fn remove_random_edge(&mut self) { 220 | let random_e_idx = self.random_edge_idx(); 221 | if random_e_idx.is_none() { 222 | return; 223 | } 224 | let endpoints = self.g.edge_endpoints(random_e_idx.unwrap()).unwrap(); 225 | 226 | self.remove_edge(endpoints.0, endpoints.1); 227 | } 228 | 229 | fn remove_edge(&mut self, start: NodeIndex, end: NodeIndex) { 230 | let (g_idx, _) = self.g.edges_connecting(start, end).next().unwrap(); 231 | self.g.remove_edge(g_idx); 232 | 233 | let sim_idx = self.sim.find_edge(start, end).unwrap(); 234 | self.sim.remove_edge(sim_idx).unwrap(); 235 | } 236 | 237 | fn draw_section_simulation(&mut self, ui: &mut Ui) { 238 | ui.horizontal_wrapped(|ui| { 239 | ui.style_mut().spacing.item_spacing = Vec2::new(0., 0.); 240 | ui.label("Force-Directed Simulation is done with "); 241 | ui.hyperlink_to("fdg project", "https://github.com/grantshandy/fdg"); 242 | }); 243 | 244 | ui.separator(); 245 | 246 | drawers::draw_start_reset_buttons( 247 | ui, 248 | drawers::ValuesConfigButtonsStartReset { 249 | simulation_stopped: self.simulation_stopped, 250 | }, 251 | |ui: &mut egui::Ui, simulation_stopped: bool, reset_pressed: bool| { 252 | self.simulation_stopped = simulation_stopped; 253 | if reset_pressed { 254 | self.reset(ui) 255 | }; 256 | }, 257 | ); 258 | 259 | ui.add_space(10.); 260 | 261 | drawers::draw_simulation_config_sliders( 262 | ui, 263 | drawers::ValuesConfigSlidersSimulation { 264 | dt: self.settings_simulation.dt, 265 | cooloff_factor: self.settings_simulation.cooloff_factor, 266 | scale: self.settings_simulation.scale, 267 | }, 268 | |delta_dt: f32, delta_cooloff_factor: f32, delta_scale: f32| { 269 | self.settings_simulation.dt += delta_dt; 270 | self.settings_simulation.cooloff_factor += delta_cooloff_factor; 271 | self.settings_simulation.scale += delta_scale; 272 | 273 | self.force = init_force(&self.settings_simulation); 274 | }, 275 | ); 276 | 277 | ui.add_space(10.); 278 | 279 | drawers::draw_counts_sliders( 280 | ui, 281 | drawers::ValuesConfigSlidersGraph { 282 | node_cnt: self.settings_graph.count_node, 283 | edge_cnt: self.settings_graph.count_edge, 284 | }, 285 | |delta_nodes, delta_edges| { 286 | self.settings_graph.count_node += delta_nodes as usize; 287 | self.settings_graph.count_edge += delta_edges as usize; 288 | 289 | if delta_nodes != 0 { 290 | if delta_nodes > 0 { 291 | (0..delta_nodes).for_each(|_| self.add_random_node()); 292 | } else { 293 | (0..delta_nodes.abs()).for_each(|_| self.remove_random_node()); 294 | } 295 | } 296 | 297 | if delta_edges != 0 { 298 | if delta_edges > 0 { 299 | (0..delta_edges).for_each(|_| self.add_random_edge()); 300 | } else { 301 | (0..delta_edges.abs()).for_each(|_| self.remove_random_edge()); 302 | } 303 | } 304 | }, 305 | ); 306 | } 307 | 308 | fn draw_section_widget(&mut self, ui: &mut Ui) { 309 | CollapsingHeader::new("Navigation") 310 | .default_open(true) 311 | .show(ui, |ui| { 312 | if ui 313 | .checkbox( 314 | &mut self.settings_navigation.fit_to_screen_enabled, 315 | "fit_to_screen", 316 | ) 317 | .changed() 318 | && self.settings_navigation.fit_to_screen_enabled 319 | { 320 | self.settings_navigation.zoom_and_pan_enabled = false 321 | }; 322 | ui.label("Enable fit to screen to fit the graph to the screen on every frame."); 323 | 324 | ui.add_space(5.); 325 | 326 | ui.add_enabled_ui(!self.settings_navigation.fit_to_screen_enabled, |ui| { 327 | ui.vertical(|ui| { 328 | ui.checkbox( 329 | &mut self.settings_navigation.zoom_and_pan_enabled, 330 | "zoom_and_pan", 331 | ); 332 | ui.label("Zoom with ctrl + mouse wheel, pan with middle mouse drag."); 333 | }) 334 | .response 335 | .on_disabled_hover_text("disable fit_to_screen to enable zoom_and_pan"); 336 | }); 337 | }); 338 | 339 | CollapsingHeader::new("Style").show(ui, |ui| { 340 | ui.checkbox(&mut self.settings_style.labels_always, "labels_always"); 341 | ui.label("Wheter to show labels always or when interacted only."); 342 | }); 343 | 344 | CollapsingHeader::new("Interaction").show(ui, |ui| { 345 | if ui.checkbox(&mut self.settings_interaction.dragging_enabled, "dragging_enabled").clicked() && self.settings_interaction.dragging_enabled { 346 | self.settings_interaction.node_clicking_enabled = true; 347 | }; 348 | ui.label("To drag use LMB click + drag on a node."); 349 | 350 | ui.add_space(5.); 351 | 352 | ui.add_enabled_ui(!(self.settings_interaction.dragging_enabled || self.settings_interaction.node_selection_enabled || self.settings_interaction.node_selection_multi_enabled), |ui| { 353 | ui.vertical(|ui| { 354 | ui.checkbox(&mut self.settings_interaction.node_clicking_enabled, "node_clicking_enabled"); 355 | ui.label("Check click events in last events"); 356 | }).response.on_disabled_hover_text("node click is enabled when any of the interaction is also enabled"); 357 | }); 358 | 359 | ui.add_space(5.); 360 | 361 | ui.add_enabled_ui(!self.settings_interaction.node_selection_multi_enabled, |ui| { 362 | ui.vertical(|ui| { 363 | if ui.checkbox(&mut self.settings_interaction.node_selection_enabled, "node_selection_enabled").clicked() && self.settings_interaction.node_selection_enabled { 364 | self.settings_interaction.node_clicking_enabled = true; 365 | }; 366 | ui.label("Enable select to select nodes with LMB click. If node is selected clicking on it again will deselect it."); 367 | }).response.on_disabled_hover_text("node_selection_multi_enabled enables select"); 368 | }); 369 | 370 | if ui.checkbox(&mut self.settings_interaction.node_selection_multi_enabled, "node_selection_multi_enabled").changed() && self.settings_interaction.node_selection_multi_enabled { 371 | self.settings_interaction.node_clicking_enabled = true; 372 | self.settings_interaction.node_selection_enabled = true; 373 | } 374 | ui.label("Enable multiselect to select multiple nodes."); 375 | 376 | ui.add_space(5.); 377 | 378 | ui.add_enabled_ui(!(self.settings_interaction.edge_selection_enabled || self.settings_interaction.edge_selection_multi_enabled), |ui| { 379 | ui.vertical(|ui| { 380 | ui.checkbox(&mut self.settings_interaction.edge_clicking_enabled, "edge_clicking_enabled"); 381 | ui.label("Check click events in last events"); 382 | }).response.on_disabled_hover_text("edge click is enabled when any of the interaction is also enabled"); 383 | }); 384 | 385 | ui.add_space(5.); 386 | 387 | ui.add_enabled_ui(!self.settings_interaction.edge_selection_multi_enabled, |ui| { 388 | ui.vertical(|ui| { 389 | if ui.checkbox(&mut self.settings_interaction.edge_selection_enabled, "edge_selection_enabled").clicked() && self.settings_interaction.edge_selection_enabled { 390 | self.settings_interaction.edge_clicking_enabled = true; 391 | }; 392 | ui.label("Enable select to select edges with LMB click. If edge is selected clicking on it again will deselect it."); 393 | }).response.on_disabled_hover_text("edge_selection_multi_enabled enables select"); 394 | }); 395 | 396 | if ui.checkbox(&mut self.settings_interaction.edge_selection_multi_enabled, "edge_selection_multi_enabled").changed() && self.settings_interaction.edge_selection_multi_enabled { 397 | self.settings_interaction.edge_clicking_enabled = true; 398 | self.settings_interaction.edge_selection_enabled = true; 399 | } 400 | ui.label("Enable multiselect to select multiple edges."); 401 | }); 402 | 403 | CollapsingHeader::new("Selected") 404 | .default_open(true) 405 | .show(ui, |ui| { 406 | ScrollArea::vertical() 407 | .auto_shrink([false, true]) 408 | .max_height(200.) 409 | .show(ui, |ui| { 410 | self.g.selected_nodes().iter().for_each(|node| { 411 | ui.label(format!("{node:?}")); 412 | }); 413 | self.g.selected_edges().iter().for_each(|edge| { 414 | ui.label(format!("{edge:?}")); 415 | }); 416 | }); 417 | }); 418 | 419 | CollapsingHeader::new("Last Events") 420 | .default_open(true) 421 | .show(ui, |ui| { 422 | if ui.button("clear").clicked() { 423 | self.last_events.clear(); 424 | } 425 | ScrollArea::vertical() 426 | .auto_shrink([false, true]) 427 | .show(ui, |ui| { 428 | self.last_events.iter().rev().for_each(|event| { 429 | ui.label(event); 430 | }); 431 | }); 432 | }); 433 | } 434 | 435 | fn draw_section_debug(&self, ui: &mut Ui) { 436 | drawers::draw_section_debug( 437 | ui, 438 | ValuesSectionDebug { 439 | zoom: self.zoom, 440 | pan: self.pan, 441 | fps: self.fps, 442 | }, 443 | ); 444 | } 445 | 446 | fn reset(&mut self, ui: &mut egui::Ui) { 447 | let settings_graph = settings::SettingsGraph::default(); 448 | let settings_simulation = settings::SettingsSimulation::default(); 449 | 450 | let mut g = random_graph(settings_graph.count_node, settings_graph.count_edge); 451 | 452 | let mut force = init_force(&self.settings_simulation); 453 | let mut sim = fdg::init_force_graph_uniform(g.g.clone(), 1.0); 454 | force.apply(&mut sim); 455 | g.g.node_weights_mut().for_each(|node| { 456 | let point: fdg::nalgebra::OPoint> = 457 | sim.node_weight(node.id()).unwrap().1; 458 | node.set_location(Pos2::new(point.coords.x, point.coords.y)); 459 | }); 460 | 461 | self.settings_simulation = settings_simulation; 462 | self.settings_graph = settings_graph; 463 | 464 | self.sim = sim; 465 | self.g = g; 466 | self.force = force; 467 | 468 | DefaultGraphView::clear_cache(ui); 469 | } 470 | } 471 | 472 | impl App for DemoApp { 473 | fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { 474 | egui::SidePanel::right("right_panel") 475 | .min_width(250.) 476 | .show(ctx, |ui| { 477 | ScrollArea::vertical().show(ui, |ui| { 478 | CollapsingHeader::new("Simulation") 479 | .default_open(true) 480 | .show(ui, |ui| self.draw_section_simulation(ui)); 481 | 482 | ui.add_space(10.); 483 | 484 | egui::CollapsingHeader::new("Debug") 485 | .default_open(true) 486 | .show(ui, |ui| self.draw_section_debug(ui)); 487 | 488 | ui.add_space(10.); 489 | 490 | CollapsingHeader::new("Widget") 491 | .default_open(true) 492 | .show(ui, |ui| self.draw_section_widget(ui)); 493 | }); 494 | }); 495 | 496 | egui::CentralPanel::default().show(ctx, |ui| { 497 | let settings_interaction = &egui_graphs::SettingsInteraction::new() 498 | .with_node_selection_enabled(self.settings_interaction.node_selection_enabled) 499 | .with_node_selection_multi_enabled( 500 | self.settings_interaction.node_selection_multi_enabled, 501 | ) 502 | .with_dragging_enabled(self.settings_interaction.dragging_enabled) 503 | .with_node_clicking_enabled(self.settings_interaction.node_clicking_enabled) 504 | .with_edge_clicking_enabled(self.settings_interaction.edge_clicking_enabled) 505 | .with_edge_selection_enabled(self.settings_interaction.edge_selection_enabled) 506 | .with_edge_selection_multi_enabled( 507 | self.settings_interaction.edge_selection_multi_enabled, 508 | ); 509 | let settings_navigation = &egui_graphs::SettingsNavigation::new() 510 | .with_zoom_and_pan_enabled(self.settings_navigation.zoom_and_pan_enabled) 511 | .with_fit_to_screen_enabled(self.settings_navigation.fit_to_screen_enabled) 512 | .with_zoom_speed(self.settings_navigation.zoom_speed); 513 | let settings_style = &egui_graphs::SettingsStyle::new() 514 | .with_labels_always(self.settings_style.labels_always); 515 | ui.add( 516 | &mut DefaultGraphView::new(&mut self.g) 517 | .with_interactions(settings_interaction) 518 | .with_navigations(settings_navigation) 519 | .with_styles(settings_style) 520 | .with_events(&self.event_publisher), 521 | ); 522 | }); 523 | 524 | self.handle_events(); 525 | self.sync(); 526 | self.update_simulation(); 527 | self.update_fps(); 528 | } 529 | } 530 | 531 | fn init_force(settings: &settings::SettingsSimulation) -> FruchtermanReingold { 532 | FruchtermanReingold { 533 | conf: FruchtermanReingoldConfiguration { 534 | dt: settings.dt, 535 | cooloff_factor: settings.cooloff_factor, 536 | scale: settings.scale, 537 | }, 538 | ..Default::default() 539 | } 540 | } 541 | 542 | fn main() { 543 | let native_options = eframe::NativeOptions::default(); 544 | run_native( 545 | "egui_graphs_demo", 546 | native_options, 547 | Box::new(|cc| Ok(Box::new(DemoApp::new(cc)))), 548 | ) 549 | .unwrap(); 550 | } 551 | -------------------------------------------------------------------------------- /examples/demo/src/settings.rs: -------------------------------------------------------------------------------- 1 | pub struct SettingsGraph { 2 | pub count_node: usize, 3 | pub count_edge: usize, 4 | } 5 | 6 | impl Default for SettingsGraph { 7 | fn default() -> Self { 8 | Self { 9 | count_node: 25, 10 | count_edge: 50, 11 | } 12 | } 13 | } 14 | 15 | #[derive(Default)] 16 | pub struct SettingsInteraction { 17 | pub dragging_enabled: bool, 18 | pub node_clicking_enabled: bool, 19 | pub node_selection_enabled: bool, 20 | pub node_selection_multi_enabled: bool, 21 | pub edge_clicking_enabled: bool, 22 | pub edge_selection_enabled: bool, 23 | pub edge_selection_multi_enabled: bool, 24 | } 25 | 26 | pub struct SettingsNavigation { 27 | pub fit_to_screen_enabled: bool, 28 | pub zoom_and_pan_enabled: bool, 29 | pub zoom_speed: f32, 30 | } 31 | 32 | impl Default for SettingsNavigation { 33 | fn default() -> Self { 34 | Self { 35 | zoom_speed: 0.1, 36 | fit_to_screen_enabled: true, 37 | zoom_and_pan_enabled: false, 38 | } 39 | } 40 | } 41 | 42 | #[derive(Default)] 43 | pub struct SettingsStyle { 44 | pub labels_always: bool, 45 | } 46 | 47 | pub struct SettingsSimulation { 48 | pub dt: f32, 49 | pub cooloff_factor: f32, 50 | pub scale: f32, 51 | } 52 | 53 | impl Default for SettingsSimulation { 54 | fn default() -> Self { 55 | Self { 56 | dt: 0.03, 57 | cooloff_factor: 0.85, 58 | scale: 100., 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/flex_nodes/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flex_nodes" 3 | version = "0.1.0" 4 | authors = ["Dmitrii Samsonov "] 5 | license = "MIT" 6 | edition = "2021" 7 | 8 | [dependencies] 9 | egui_graphs = { path = "../.." } 10 | egui = "0.31" 11 | eframe = "0.31" 12 | petgraph = "0.8" -------------------------------------------------------------------------------- /examples/flex_nodes/README.md: -------------------------------------------------------------------------------- 1 | # Flex nodes 2 | 3 | Example demonstrates how to use custom drawing functions for nodes with flexible size which depends on their label size. 4 | 5 | ## run 6 | 7 | ```bash 8 | cargo run --release -p flex_nodes 9 | ``` 10 | -------------------------------------------------------------------------------- /examples/flex_nodes/src/main.rs: -------------------------------------------------------------------------------- 1 | use eframe::{run_native, App, CreationContext}; 2 | use egui::{CentralPanel, Context, SidePanel, TextEdit}; 3 | use egui_graphs::{Graph, GraphView, SettingsInteraction, SettingsNavigation}; 4 | use node::NodeShapeFlex; 5 | use petgraph::{ 6 | stable_graph::{DefaultIx, EdgeIndex, NodeIndex, StableGraph}, 7 | Directed, 8 | }; 9 | 10 | mod node; 11 | 12 | pub struct FlexNodesApp { 13 | g: Graph<(), (), Directed, DefaultIx, NodeShapeFlex>, 14 | label_input: String, 15 | selected_node: Option, 16 | selected_edge: Option, 17 | } 18 | 19 | impl FlexNodesApp { 20 | fn new(_: &CreationContext<'_>) -> Self { 21 | let g = generate_graph(); 22 | Self { 23 | g: Graph::from(&g), 24 | label_input: String::default(), 25 | selected_node: Option::default(), 26 | selected_edge: Option::default(), 27 | } 28 | } 29 | 30 | fn read_data(&mut self) { 31 | if !self.g.selected_nodes().is_empty() { 32 | let idx = self.g.selected_nodes().first().unwrap(); 33 | self.selected_node = Some(*idx); 34 | self.selected_edge = None; 35 | self.label_input = self.g.node(*idx).unwrap().label(); 36 | } 37 | if !self.g.selected_edges().is_empty() { 38 | let idx = self.g.selected_edges().first().unwrap(); 39 | self.selected_edge = Some(*idx); 40 | self.selected_node = None; 41 | self.label_input = self.g.edge(*idx).unwrap().label(); 42 | } 43 | } 44 | 45 | fn render(&mut self, ctx: &Context) { 46 | SidePanel::right("right_panel").show(ctx, |ui| { 47 | ui.label("Change Label"); 48 | ui.add_enabled_ui( 49 | self.selected_node.is_some() || self.selected_edge.is_some(), 50 | |ui| { 51 | TextEdit::singleline(&mut self.label_input) 52 | .hint_text("select node or edge") 53 | .show(ui) 54 | }, 55 | ); 56 | if ui.button("reset").clicked() { 57 | self.reset() 58 | } 59 | }); 60 | CentralPanel::default().show(ctx, |ui| { 61 | let widget = &mut GraphView::<_, _, _, _, _, _>::new(&mut self.g) 62 | .with_interactions( 63 | &SettingsInteraction::default() 64 | .with_dragging_enabled(true) 65 | .with_node_selection_enabled(true) 66 | .with_edge_selection_enabled(true), 67 | ) 68 | .with_navigations( 69 | &SettingsNavigation::default() 70 | .with_fit_to_screen_enabled(false) 71 | .with_zoom_and_pan_enabled(true), 72 | ); 73 | ui.add(widget); 74 | }); 75 | } 76 | 77 | fn update_data(&mut self) { 78 | if self.selected_node.is_none() && self.selected_edge.is_none() { 79 | return; 80 | } 81 | 82 | if self.selected_node.is_some() { 83 | let idx = self.selected_node.unwrap(); 84 | if idx.index().to_string() == self.label_input { 85 | return; 86 | } 87 | 88 | self.g 89 | .node_mut(idx) 90 | .unwrap() 91 | .set_label(self.label_input.clone()); 92 | } 93 | 94 | if self.selected_edge.is_some() { 95 | let idx = self.selected_edge.unwrap(); 96 | if idx.index().to_string() == self.label_input { 97 | return; 98 | } 99 | 100 | self.g 101 | .edge_mut(idx) 102 | .unwrap() 103 | .set_label(self.label_input.clone()); 104 | } 105 | } 106 | 107 | fn reset(&mut self) { 108 | let g = generate_graph(); 109 | *self = Self { 110 | g: Graph::from(&g), 111 | label_input: String::default(), 112 | selected_node: Option::default(), 113 | selected_edge: Option::default(), 114 | }; 115 | } 116 | } 117 | 118 | impl App for FlexNodesApp { 119 | fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { 120 | self.read_data(); 121 | self.render(ctx); 122 | self.update_data(); 123 | } 124 | } 125 | 126 | fn generate_graph() -> StableGraph<(), ()> { 127 | let mut g = StableGraph::new(); 128 | 129 | let a = g.add_node(()); 130 | let b = g.add_node(()); 131 | let c = g.add_node(()); 132 | 133 | g.add_edge(a, b, ()); 134 | g.add_edge(b, c, ()); 135 | g.add_edge(c, a, ()); 136 | 137 | g 138 | } 139 | 140 | fn main() { 141 | let native_options = eframe::NativeOptions::default(); 142 | run_native( 143 | "egui_graphs_flex_nodes_demo", 144 | native_options, 145 | Box::new(|cc| Ok(Box::new(FlexNodesApp::new(cc)))), 146 | ) 147 | .unwrap(); 148 | } 149 | -------------------------------------------------------------------------------- /examples/flex_nodes/src/node.rs: -------------------------------------------------------------------------------- 1 | use egui::{epaint::TextShape, Color32, FontFamily, FontId, Pos2, Rect, Shape, Stroke, Vec2}; 2 | use egui_graphs::{DisplayNode, NodeProps}; 3 | use petgraph::{stable_graph::IndexType, EdgeType}; 4 | 5 | #[derive(Clone)] 6 | pub struct NodeShapeFlex { 7 | label: String, 8 | loc: Pos2, 9 | 10 | size_x: f32, 11 | size_y: f32, 12 | } 13 | 14 | impl From> for NodeShapeFlex { 15 | fn from(node_props: NodeProps) -> Self { 16 | Self { 17 | label: node_props.label.clone(), 18 | loc: node_props.location(), 19 | 20 | size_x: 0., 21 | size_y: 0., 22 | } 23 | } 24 | } 25 | 26 | impl DisplayNode for NodeShapeFlex { 27 | fn is_inside(&self, pos: Pos2) -> bool { 28 | let rect = Rect::from_center_size(self.loc, Vec2::new(self.size_x, self.size_y)); 29 | 30 | rect.contains(pos) 31 | } 32 | 33 | fn closest_boundary_point(&self, dir: Vec2) -> Pos2 { 34 | find_intersection(self.loc, self.size_x / 2., self.size_y / 2., dir) 35 | } 36 | 37 | fn shapes(&mut self, ctx: &egui_graphs::DrawContext) -> Vec { 38 | // find node center location on the screen coordinates 39 | let center = ctx.meta.canvas_to_screen_pos(self.loc); 40 | let color = ctx.ctx.style().visuals.text_color(); 41 | 42 | // create label 43 | let galley = ctx.ctx.fonts(|f| { 44 | f.layout_no_wrap( 45 | self.label.clone(), 46 | FontId::new(ctx.meta.canvas_to_screen_size(10.), FontFamily::Monospace), 47 | color, 48 | ) 49 | }); 50 | 51 | // we need to offset label by half its size to place it in the center of the rect 52 | let offset = Vec2::new(-galley.size().x / 2., -galley.size().y / 2.); 53 | 54 | // create the shape and add it to the layers 55 | let shape_label = TextShape::new(center + offset, galley, color); 56 | 57 | let rect = shape_label.visual_bounding_rect(); 58 | let points = rect_to_points(rect); 59 | let shape_rect = Shape::convex_polygon(points, Color32::default(), Stroke::new(1., color)); 60 | 61 | // update self size 62 | self.size_x = rect.size().x; 63 | self.size_y = rect.size().y; 64 | 65 | vec![shape_rect, shape_label.into()] 66 | } 67 | 68 | fn update(&mut self, state: &NodeProps) { 69 | self.label = state.label.clone(); 70 | self.loc = state.location(); 71 | } 72 | } 73 | 74 | fn find_intersection(center: Pos2, size_x: f32, size_y: f32, direction: Vec2) -> Pos2 { 75 | if (direction.x.abs() * size_y) > (direction.y.abs() * size_x) { 76 | // intersects left or right side 77 | let x = if direction.x > 0.0 { 78 | center.x + size_x / 2.0 79 | } else { 80 | center.x - size_x / 2.0 81 | }; 82 | let y = center.y + direction.y / direction.x * (x - center.x); 83 | Pos2::new(x, y) 84 | } else { 85 | // intersects top or bottom side 86 | let y = if direction.y > 0.0 { 87 | center.y + size_y / 2.0 88 | } else { 89 | center.y - size_y / 2.0 90 | }; 91 | let x = center.x + direction.x / direction.y * (y - center.y); 92 | Pos2::new(x, y) 93 | } 94 | } 95 | 96 | fn rect_to_points(rect: Rect) -> Vec { 97 | let top_left = rect.min; 98 | let bottom_right = rect.max; 99 | let top_right = Pos2::new(bottom_right.x, top_left.y); 100 | let bottom_left = Pos2::new(top_left.x, bottom_right.y); 101 | 102 | vec![top_left, top_right, bottom_right, bottom_left] 103 | } 104 | -------------------------------------------------------------------------------- /examples/interactive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "interactive" 3 | version = "0.1.0" 4 | authors = ["Dmitrii Samsonov "] 5 | license = "MIT" 6 | edition = "2021" 7 | 8 | [dependencies] 9 | egui_graphs = { path = "../.." } 10 | egui = "0.31" 11 | eframe = "0.31" 12 | petgraph = "0.8" -------------------------------------------------------------------------------- /examples/interactive/README.md: -------------------------------------------------------------------------------- 1 | # Interactive 2 | 3 | Modification of the [basic example](https://github.com/blitzarx1/egui_graph/tree/master/examples/basic) which shows how easy it is to enable interactivity. 4 | 5 | Try to drag around and select some nodes. 6 | 7 | ## run 8 | 9 | ```bash 10 | cargo run --release -p interactive 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/interactive/src/main.rs: -------------------------------------------------------------------------------- 1 | use eframe::{run_native, App, CreationContext}; 2 | use egui::Context; 3 | use egui_graphs::{ 4 | DefaultGraphView, Graph, SettingsInteraction, SettingsNavigation, SettingsStyle, 5 | }; 6 | use petgraph::stable_graph::StableGraph; 7 | 8 | pub struct InteractiveApp { 9 | g: Graph, 10 | } 11 | 12 | impl InteractiveApp { 13 | fn new(_: &CreationContext<'_>) -> Self { 14 | let g: Graph = generate_graph(); 15 | Self { g } 16 | } 17 | } 18 | 19 | impl App for InteractiveApp { 20 | fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { 21 | egui::CentralPanel::default().show(ctx, |ui| { 22 | let interaction_settings = &SettingsInteraction::new() 23 | .with_dragging_enabled(true) 24 | .with_node_clicking_enabled(true) 25 | .with_node_selection_enabled(true) 26 | .with_node_selection_multi_enabled(true) 27 | .with_edge_clicking_enabled(true) 28 | .with_edge_selection_enabled(true) 29 | .with_edge_selection_multi_enabled(true); 30 | let style_settings = &SettingsStyle::new().with_labels_always(true); 31 | let navigation_settings = &SettingsNavigation::new() 32 | .with_fit_to_screen_enabled(false) 33 | .with_zoom_and_pan_enabled(true); 34 | 35 | ui.add( 36 | &mut DefaultGraphView::new(&mut self.g) 37 | .with_styles(style_settings) 38 | .with_interactions(interaction_settings) 39 | .with_navigations(navigation_settings), 40 | ); 41 | }); 42 | } 43 | } 44 | 45 | fn generate_graph() -> Graph { 46 | let mut g = StableGraph::new(); 47 | 48 | let a = g.add_node(()); 49 | let b = g.add_node(()); 50 | let c = g.add_node(()); 51 | 52 | g.add_edge(a, a, ()); 53 | g.add_edge(a, b, ()); 54 | g.add_edge(a, b, ()); 55 | g.add_edge(c, b, ()); 56 | g.add_edge(b, c, ()); 57 | g.add_edge(c, a, ()); 58 | 59 | Graph::from(&g) 60 | } 61 | 62 | fn main() { 63 | let native_options = eframe::NativeOptions::default(); 64 | run_native( 65 | "egui_graphs_interactive_demo", 66 | native_options, 67 | Box::new(|cc| Ok(Box::new(InteractiveApp::new(cc)))), 68 | ) 69 | .unwrap(); 70 | } 71 | -------------------------------------------------------------------------------- /examples/label_change/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "label_change" 3 | version = "0.1.0" 4 | authors = ["Dmitrii Samsonov "] 5 | license = "MIT" 6 | edition = "2021" 7 | 8 | [dependencies] 9 | egui_graphs = { path = "../.." } 10 | egui = "0.31" 11 | eframe = "0.31" 12 | petgraph = "0.8" -------------------------------------------------------------------------------- /examples/label_change/README.md: -------------------------------------------------------------------------------- 1 | # Label Change 2 | 3 | Simple example which demonstrates the API for changing node props including specifically `label`. 4 | 5 | ## run 6 | 7 | ```bash 8 | cargo run --release -p label_change 9 | ``` 10 | -------------------------------------------------------------------------------- /examples/label_change/src/main.rs: -------------------------------------------------------------------------------- 1 | use eframe::{run_native, App, CreationContext}; 2 | use egui::{CentralPanel, Context, SidePanel, TextEdit}; 3 | use egui_graphs::{DefaultGraphView, Graph, SettingsInteraction, SettingsStyle}; 4 | use petgraph::stable_graph::{EdgeIndex, NodeIndex, StableGraph}; 5 | 6 | pub struct LabelChangeApp { 7 | g: Graph, 8 | label_input: String, 9 | selected_node: Option, 10 | selected_edge: Option, 11 | } 12 | 13 | impl LabelChangeApp { 14 | fn new(_: &CreationContext<'_>) -> Self { 15 | let g = generate_graph(); 16 | Self { 17 | g: Graph::from(&g), 18 | label_input: String::default(), 19 | selected_node: Option::default(), 20 | selected_edge: Option::default(), 21 | } 22 | } 23 | 24 | fn read_data(&mut self) { 25 | if !self.g.selected_nodes().is_empty() { 26 | let idx = self.g.selected_nodes().first().unwrap(); 27 | self.selected_node = Some(*idx); 28 | self.selected_edge = None; 29 | self.label_input = self.g.node(*idx).unwrap().label(); 30 | } 31 | if !self.g.selected_edges().is_empty() { 32 | let idx = self.g.selected_edges().first().unwrap(); 33 | self.selected_edge = Some(*idx); 34 | self.selected_node = None; 35 | self.label_input = self.g.edge(*idx).unwrap().label(); 36 | } 37 | } 38 | 39 | fn render(&mut self, ctx: &Context) { 40 | SidePanel::right("right_panel").show(ctx, |ui| { 41 | ui.label("Change Label"); 42 | ui.add_enabled_ui( 43 | self.selected_node.is_some() || self.selected_edge.is_some(), 44 | |ui| { 45 | TextEdit::singleline(&mut self.label_input) 46 | .hint_text("select node or edge") 47 | .show(ui) 48 | }, 49 | ); 50 | if ui.button("reset").clicked() { 51 | self.reset() 52 | } 53 | }); 54 | CentralPanel::default().show(ctx, |ui| { 55 | let widget = &mut DefaultGraphView::new(&mut self.g) 56 | .with_interactions( 57 | &SettingsInteraction::default() 58 | .with_node_selection_enabled(true) 59 | .with_edge_selection_enabled(true), 60 | ) 61 | .with_styles(&SettingsStyle::default().with_labels_always(true)); 62 | ui.add(widget); 63 | }); 64 | } 65 | 66 | fn update_data(&mut self) { 67 | if self.selected_node.is_none() && self.selected_edge.is_none() { 68 | return; 69 | } 70 | 71 | if self.selected_node.is_some() { 72 | let idx = self.selected_node.unwrap(); 73 | if idx.index().to_string() == self.label_input { 74 | return; 75 | } 76 | 77 | self.g 78 | .node_mut(idx) 79 | .unwrap() 80 | .set_label(self.label_input.clone()); 81 | } 82 | 83 | if self.selected_edge.is_some() { 84 | let idx = self.selected_edge.unwrap(); 85 | if idx.index().to_string() == self.label_input { 86 | return; 87 | } 88 | 89 | self.g 90 | .edge_mut(idx) 91 | .unwrap() 92 | .set_label(self.label_input.clone()); 93 | } 94 | } 95 | 96 | fn reset(&mut self) { 97 | let g = generate_graph(); 98 | *self = Self { 99 | g: Graph::from(&g), 100 | label_input: String::default(), 101 | selected_node: Option::default(), 102 | selected_edge: Option::default(), 103 | }; 104 | } 105 | } 106 | 107 | impl App for LabelChangeApp { 108 | fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { 109 | self.read_data(); 110 | self.render(ctx); 111 | self.update_data(); 112 | } 113 | } 114 | 115 | fn generate_graph() -> StableGraph<(), ()> { 116 | let mut g = StableGraph::new(); 117 | 118 | let a = g.add_node(()); 119 | let b = g.add_node(()); 120 | let c = g.add_node(()); 121 | 122 | g.add_edge(a, b, ()); 123 | g.add_edge(b, c, ()); 124 | g.add_edge(c, a, ()); 125 | 126 | g 127 | } 128 | 129 | fn main() { 130 | let native_options = eframe::NativeOptions::default(); 131 | run_native( 132 | "egui_graphs_label_change_demo", 133 | native_options, 134 | Box::new(|cc| Ok(Box::new(LabelChangeApp::new(cc)))), 135 | ) 136 | .unwrap(); 137 | } 138 | -------------------------------------------------------------------------------- /examples/layouts/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "layouts" 3 | version = "0.1.0" 4 | authors = ["Dmitrii Samsonov "] 5 | license = "MIT" 6 | edition = "2021" 7 | 8 | [dependencies] 9 | egui_graphs = { path = "../.." } 10 | egui = "0.31" 11 | eframe = "0.31" 12 | petgraph = "0.8" -------------------------------------------------------------------------------- /examples/layouts/README.md: -------------------------------------------------------------------------------- 1 | # Layouts 2 | 3 | Example of usage of different lyouts implemented by `egui_graphs`. 4 | 5 | ## run 6 | 7 | ```bash 8 | cargo run --release -p layouts 9 | ``` 10 | -------------------------------------------------------------------------------- /examples/layouts/src/main.rs: -------------------------------------------------------------------------------- 1 | use eframe::{run_native, App, CreationContext, NativeOptions}; 2 | use egui::Context; 3 | use egui_graphs::{ 4 | random_graph, DefaultEdgeShape, DefaultNodeShape, Graph, GraphView, LayoutHierarchical, 5 | LayoutRandom, LayoutStateHierarchical, LayoutStateRandom, 6 | }; 7 | use petgraph::{stable_graph::DefaultIx, Directed}; 8 | 9 | #[derive(Clone, PartialEq)] 10 | enum Layout { 11 | Hierarchical, 12 | Random, 13 | } 14 | 15 | #[derive(Clone)] 16 | struct Settings { 17 | layout: Layout, 18 | num_nodes: usize, 19 | num_edges: usize, 20 | } 21 | pub struct LayoutsApp { 22 | settings: Settings, 23 | g: Graph, 24 | } 25 | 26 | impl LayoutsApp { 27 | fn new(_: &CreationContext<'_>) -> Self { 28 | let settings = Settings { 29 | layout: Layout::Hierarchical, 30 | num_nodes: 25, 31 | num_edges: 25, 32 | }; 33 | Self { 34 | settings: settings.clone(), 35 | g: random_graph(settings.num_nodes, settings.num_edges), 36 | } 37 | } 38 | 39 | fn reset(&mut self, ui: &mut egui::Ui) { 40 | match self.settings.layout { 41 | Layout::Hierarchical => { 42 | GraphView::< 43 | (), 44 | (), 45 | Directed, 46 | DefaultIx, 47 | DefaultNodeShape, 48 | DefaultEdgeShape, 49 | LayoutStateHierarchical, 50 | LayoutHierarchical, 51 | >::reset(ui); 52 | } 53 | Layout::Random => { 54 | GraphView::< 55 | (), 56 | (), 57 | Directed, 58 | DefaultIx, 59 | DefaultNodeShape, 60 | DefaultEdgeShape, 61 | LayoutStateRandom, 62 | LayoutRandom, 63 | >::reset(ui); 64 | } 65 | }; 66 | } 67 | } 68 | 69 | impl App for LayoutsApp { 70 | fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { 71 | egui::SidePanel::right("right_panel") 72 | .min_width(250.) 73 | .show(ctx, |ui| { 74 | ui.vertical(|ui| { 75 | ui.horizontal(|ui| { 76 | ui.label("Layout"); 77 | if ui 78 | .radio_value( 79 | &mut self.settings.layout, 80 | Layout::Hierarchical, 81 | "Hierarchical", 82 | ) 83 | .changed() 84 | { 85 | self.reset(ui); 86 | }; 87 | if ui 88 | .radio_value(&mut self.settings.layout, Layout::Random, "Random") 89 | .changed() 90 | { 91 | self.reset(ui); 92 | }; 93 | }); 94 | ui.horizontal(|ui| { 95 | ui.label("Number of nodes"); 96 | if ui 97 | .add(egui::Slider::new(&mut self.settings.num_nodes, 1..=250)) 98 | .changed() 99 | { 100 | self.reset(ui); 101 | self.g = random_graph(self.settings.num_nodes, self.settings.num_edges); 102 | }; 103 | }); 104 | ui.horizontal(|ui| { 105 | ui.label("Number of edges"); 106 | if ui 107 | .add(egui::Slider::new(&mut self.settings.num_edges, 1..=250)) 108 | .changed() 109 | { 110 | self.reset(ui); 111 | self.g = random_graph(self.settings.num_nodes, self.settings.num_edges); 112 | }; 113 | }); 114 | }); 115 | }); 116 | egui::CentralPanel::default().show(ctx, |ui| { 117 | match self.settings.layout { 118 | Layout::Hierarchical => { 119 | ui.add(&mut GraphView::< 120 | _, 121 | _, 122 | _, 123 | _, 124 | _, 125 | _, 126 | LayoutStateHierarchical, 127 | LayoutHierarchical, 128 | >::new(&mut self.g)); 129 | } 130 | Layout::Random => { 131 | ui.add(&mut GraphView::< 132 | _, 133 | _, 134 | _, 135 | _, 136 | _, 137 | _, 138 | LayoutStateRandom, 139 | LayoutRandom, 140 | >::new(&mut self.g)); 141 | } 142 | }; 143 | }); 144 | } 145 | } 146 | 147 | fn main() { 148 | run_native( 149 | "egui_graphs_layouts_demo", 150 | NativeOptions::default(), 151 | Box::new(|cc| Ok(Box::new(LayoutsApp::new(cc)))), 152 | ) 153 | .unwrap(); 154 | } 155 | -------------------------------------------------------------------------------- /examples/multiple/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "multiple" 3 | version = "0.1.0" 4 | authors = ["Dmitrii Samsonov "] 5 | license = "MIT" 6 | edition = "2021" 7 | 8 | [dependencies] 9 | egui_graphs = { path = "../.." } 10 | egui = "0.31" 11 | eframe = "0.31" 12 | petgraph = "0.8" -------------------------------------------------------------------------------- /examples/multiple/README.md: -------------------------------------------------------------------------------- 1 | # Multiple 2 | 3 | Example which demonstrates the usage of `GraphView` widget in multiple egui tabs. Please pay attention that there is one graph and multiple `GraphView` widgets - this means that modifying graph in one widget causes modification in others. 4 | 5 | ## run 6 | 7 | ```bash 8 | cargo run --release -p multiple 9 | ``` 10 | -------------------------------------------------------------------------------- /examples/multiple/src/main.rs: -------------------------------------------------------------------------------- 1 | use eframe::{run_native, App, CreationContext, Frame}; 2 | use egui::{CentralPanel, Context, Layout, SidePanel}; 3 | use egui_graphs::{DefaultGraphView, Graph, SettingsInteraction, SettingsNavigation}; 4 | use petgraph::stable_graph::StableGraph; 5 | 6 | pub struct BasicApp { 7 | g: Graph, 8 | } 9 | 10 | impl BasicApp { 11 | fn new(_: &CreationContext<'_>) -> Self { 12 | let g = generate_graph(); 13 | Self { g: Graph::from(&g) } 14 | } 15 | } 16 | 17 | impl App for BasicApp { 18 | fn update(&mut self, ctx: &Context, _: &mut Frame) { 19 | let available_width = ctx.available_rect().width(); 20 | SidePanel::left("left_panel") 21 | .default_width(available_width / 3.) 22 | .resizable(true) 23 | .show(ctx, |ui| { 24 | ui.allocate_ui_with_layout(ui.max_rect().size(), Layout::default(), |ui| { 25 | ui.add( 26 | &mut DefaultGraphView::new(&mut self.g) 27 | .with_navigations( 28 | &SettingsNavigation::default() 29 | .with_fit_to_screen_enabled(false) 30 | .with_zoom_and_pan_enabled(true), 31 | ) 32 | .with_interactions( 33 | &SettingsInteraction::default() 34 | .with_node_selection_multi_enabled(true) 35 | .with_node_selection_enabled(true) 36 | .with_edge_selection_enabled(true) 37 | .with_edge_selection_multi_enabled(true) 38 | .with_dragging_enabled(true), 39 | ), 40 | ); 41 | }); 42 | }); 43 | SidePanel::right("right_panel") 44 | .default_width(available_width / 3.) 45 | .resizable(true) 46 | .show(ctx, |ui| { 47 | ui.add( 48 | &mut DefaultGraphView::new(&mut self.g) 49 | .with_navigations( 50 | &SettingsNavigation::default() 51 | .with_fit_to_screen_enabled(false) 52 | .with_zoom_and_pan_enabled(true), 53 | ) 54 | .with_interactions( 55 | &SettingsInteraction::default() 56 | .with_node_selection_multi_enabled(true) 57 | .with_node_selection_enabled(true) 58 | .with_edge_selection_enabled(true) 59 | .with_edge_selection_multi_enabled(true) 60 | .with_dragging_enabled(true), 61 | ), 62 | ) 63 | }); 64 | CentralPanel::default().show(ctx, |ui| { 65 | ui.add( 66 | &mut DefaultGraphView::new(&mut self.g) 67 | .with_navigations( 68 | &SettingsNavigation::default() 69 | .with_fit_to_screen_enabled(false) 70 | .with_zoom_and_pan_enabled(true), 71 | ) 72 | .with_interactions( 73 | &SettingsInteraction::default() 74 | .with_node_selection_multi_enabled(true) 75 | .with_node_selection_enabled(true) 76 | .with_edge_selection_enabled(true) 77 | .with_edge_selection_multi_enabled(true) 78 | .with_dragging_enabled(true), 79 | ), 80 | ) 81 | }); 82 | } 83 | } 84 | 85 | fn generate_graph() -> StableGraph<(), ()> { 86 | let mut g = StableGraph::new(); 87 | 88 | let a = g.add_node(()); 89 | let b = g.add_node(()); 90 | let c = g.add_node(()); 91 | 92 | g.add_edge(a, b, ()); 93 | g.add_edge(b, c, ()); 94 | g.add_edge(c, a, ()); 95 | 96 | g 97 | } 98 | 99 | fn main() { 100 | let native_options = eframe::NativeOptions::default(); 101 | run_native( 102 | "egui_graphs_multiple_demo", 103 | native_options, 104 | Box::new(|cc| Ok(Box::new(BasicApp::new(cc)))), 105 | ) 106 | .unwrap(); 107 | } 108 | -------------------------------------------------------------------------------- /examples/rainbow_edges/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rainbow_edges" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | egui_graphs = { path = "../.." } 8 | egui = "0.31" 9 | eframe = "0.31" 10 | petgraph = "0.8" -------------------------------------------------------------------------------- /examples/rainbow_edges/README.md: -------------------------------------------------------------------------------- 1 | # Rainbow Edges 2 | 3 | Example demonstrates the possibility of drawing custom edges shapes. 4 | 5 | ## run 6 | 7 | ```bash 8 | cargo run --release -p rainbow_edges 9 | ``` 10 | -------------------------------------------------------------------------------- /examples/rainbow_edges/src/edge.rs: -------------------------------------------------------------------------------- 1 | use egui::{Color32, Pos2, Shape, Stroke, Vec2}; 2 | use egui_graphs::{DefaultEdgeShape, DisplayEdge, DisplayNode, DrawContext, EdgeProps, Node}; 3 | use petgraph::{stable_graph::IndexType, EdgeType}; 4 | 5 | const TIP_ANGLE: f32 = std::f32::consts::TAU / 30.; 6 | const TIP_SIZE: f32 = 15.; 7 | const COLORS: [Color32; 7] = [ 8 | Color32::RED, 9 | Color32::from_rgb(255, 102, 0), 10 | Color32::YELLOW, 11 | Color32::GREEN, 12 | Color32::from_rgb(2, 216, 233), 13 | Color32::BLUE, 14 | Color32::from_rgb(91, 10, 145), 15 | ]; 16 | 17 | #[derive(Clone)] 18 | pub struct RainbowEdgeShape { 19 | default_impl: DefaultEdgeShape, 20 | } 21 | 22 | impl From> for RainbowEdgeShape { 23 | fn from(props: EdgeProps) -> Self { 24 | Self { 25 | default_impl: DefaultEdgeShape::from(props), 26 | } 27 | } 28 | } 29 | 30 | impl> 31 | DisplayEdge for RainbowEdgeShape 32 | { 33 | fn shapes( 34 | &mut self, 35 | start: &Node, 36 | end: &Node, 37 | ctx: &DrawContext, 38 | ) -> Vec { 39 | let mut res = vec![]; 40 | let (start, end) = (start.location(), end.location()); 41 | let (x_dist, y_dist) = (end.x - start.x, end.y - start.y); 42 | let (dx, dy) = (x_dist / COLORS.len() as f32, y_dist / COLORS.len() as f32); 43 | let d_vec = Vec2::new(dx, dy); 44 | 45 | let mut stroke = Stroke::default(); 46 | let mut points_line; 47 | 48 | for (i, color) in COLORS.iter().enumerate() { 49 | stroke = Stroke::new(self.default_impl.width, *color); 50 | points_line = vec![ 51 | start + i as f32 * d_vec, 52 | end - (COLORS.len() - i - 1) as f32 * d_vec, 53 | ]; 54 | 55 | stroke.width = ctx.meta.canvas_to_screen_size(stroke.width); 56 | points_line = points_line 57 | .iter() 58 | .map(|p| ctx.meta.canvas_to_screen_pos(*p)) 59 | .collect(); 60 | res.push(Shape::line_segment( 61 | [points_line[0], points_line[1]], 62 | stroke, 63 | )); 64 | } 65 | 66 | let tip_dir = (end - start).normalized(); 67 | 68 | let arrow_tip_dir_1 = rotate_vector(tip_dir, TIP_ANGLE) * TIP_SIZE; 69 | let arrow_tip_dir_2 = rotate_vector(tip_dir, -TIP_ANGLE) * TIP_SIZE; 70 | 71 | let tip_start_1 = end - arrow_tip_dir_1; 72 | let tip_start_2 = end - arrow_tip_dir_2; 73 | 74 | let mut points_tip = vec![end, tip_start_1, tip_start_2]; 75 | 76 | points_tip = points_tip 77 | .iter() 78 | .map(|p| ctx.meta.canvas_to_screen_pos(*p)) 79 | .collect(); 80 | 81 | res.push(Shape::convex_polygon( 82 | points_tip, 83 | stroke.color, 84 | Stroke::default(), 85 | )); 86 | 87 | res 88 | } 89 | 90 | fn update(&mut self, _: &egui_graphs::EdgeProps) {} 91 | 92 | fn is_inside( 93 | &self, 94 | start: &Node, 95 | end: &Node, 96 | pos: Pos2, 97 | ) -> bool { 98 | self.default_impl.is_inside(start, end, pos) 99 | } 100 | } 101 | 102 | /// rotates vector by angle 103 | fn rotate_vector(vec: Vec2, angle: f32) -> Vec2 { 104 | let cos = angle.cos(); 105 | let sin = angle.sin(); 106 | Vec2::new(cos * vec.x - sin * vec.y, sin * vec.x + cos * vec.y) 107 | } 108 | -------------------------------------------------------------------------------- /examples/rainbow_edges/src/main.rs: -------------------------------------------------------------------------------- 1 | use edge::RainbowEdgeShape; 2 | use eframe::{run_native, App, CreationContext}; 3 | use egui::Context; 4 | use egui_graphs::{DefaultNodeShape, Graph, GraphView}; 5 | use petgraph::{csr::DefaultIx, stable_graph::StableGraph, Directed}; 6 | 7 | mod edge; 8 | 9 | pub struct RainbowEdgesApp { 10 | g: Graph<(), (), Directed, DefaultIx, DefaultNodeShape, RainbowEdgeShape>, 11 | } 12 | 13 | impl RainbowEdgesApp { 14 | fn new(_: &CreationContext<'_>) -> Self { 15 | let g = generate_graph(); 16 | Self { g: Graph::from(&g) } 17 | } 18 | } 19 | 20 | impl App for RainbowEdgesApp { 21 | fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { 22 | egui::CentralPanel::default().show(ctx, |ui| { 23 | ui.add( 24 | &mut GraphView::<_, _, _, _, DefaultNodeShape, RainbowEdgeShape>::new(&mut self.g) 25 | .with_interactions( 26 | &egui_graphs::SettingsInteraction::default().with_dragging_enabled(true), 27 | ), 28 | ); 29 | }); 30 | } 31 | } 32 | 33 | fn generate_graph() -> StableGraph<(), ()> { 34 | let mut g = StableGraph::new(); 35 | 36 | let a = g.add_node(()); 37 | let b = g.add_node(()); 38 | let c = g.add_node(()); 39 | 40 | g.add_edge(a, b, ()); 41 | g.add_edge(b, c, ()); 42 | g.add_edge(c, a, ()); 43 | 44 | g 45 | } 46 | 47 | fn main() { 48 | let native_options = eframe::NativeOptions::default(); 49 | run_native( 50 | "egui_graphs_rainbow_edges_demo", 51 | native_options, 52 | Box::new(|cc| Ok(Box::new(RainbowEdgesApp::new(cc)))), 53 | ) 54 | .unwrap(); 55 | } 56 | -------------------------------------------------------------------------------- /examples/undirected/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "undirected" 3 | version = "0.1.0" 4 | authors = ["Dmitrii Samsonov "] 5 | license = "MIT" 6 | edition = "2021" 7 | 8 | [dependencies] 9 | egui_graphs = { path = "../.." } 10 | egui = "0.31" 11 | eframe = "0.31" 12 | petgraph = "0.8" -------------------------------------------------------------------------------- /examples/undirected/README.md: -------------------------------------------------------------------------------- 1 | # Undirected 2 | 3 | Basic example which demonstrates the usage of `GraphView` widget with undirected graph. 4 | 5 | ## run 6 | 7 | ```bash 8 | cargo run --release -p undirected 9 | ``` 10 | -------------------------------------------------------------------------------- /examples/undirected/src/main.rs: -------------------------------------------------------------------------------- 1 | use eframe::{run_native, App, CreationContext}; 2 | use egui::Context; 3 | use egui_graphs::{Graph, GraphView, LayoutRandom, LayoutStateRandom}; 4 | use petgraph::{ 5 | stable_graph::{StableGraph, StableUnGraph}, 6 | Undirected, 7 | }; 8 | 9 | pub struct UndirectedApp { 10 | g: Graph<(), (), Undirected>, 11 | } 12 | 13 | impl UndirectedApp { 14 | fn new(_: &CreationContext<'_>) -> Self { 15 | let g = generate_graph(); 16 | Self { g: Graph::from(&g) } 17 | } 18 | } 19 | 20 | impl App for UndirectedApp { 21 | fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { 22 | egui::CentralPanel::default().show(ctx, |ui| { 23 | ui.add(&mut GraphView::< 24 | _, 25 | _, 26 | _, 27 | _, 28 | _, 29 | _, 30 | LayoutStateRandom, 31 | LayoutRandom, 32 | >::new(&mut self.g)); 33 | }); 34 | } 35 | } 36 | 37 | fn generate_graph() -> StableGraph<(), (), Undirected> { 38 | let mut g = StableUnGraph::default(); 39 | 40 | let a = g.add_node(()); 41 | let b = g.add_node(()); 42 | let c = g.add_node(()); 43 | 44 | g.add_edge(a, b, ()); 45 | g.add_edge(b, c, ()); 46 | g.add_edge(c, a, ()); 47 | 48 | g 49 | } 50 | 51 | fn main() { 52 | let native_options = eframe::NativeOptions::default(); 53 | run_native( 54 | "egui_graphs_undirected_demo", 55 | native_options, 56 | Box::new(|cc| Ok(Box::new(UndirectedApp::new(cc)))), 57 | ) 58 | .unwrap(); 59 | } 60 | -------------------------------------------------------------------------------- /examples/wasm_custom_draw/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm_custom_draw" 3 | version = "0.1.0" 4 | authors = ["Atlas16A"] 5 | license = "MIT" 6 | edition = "2021" 7 | 8 | [dependencies] 9 | egui_graphs = { path = "../.." } 10 | egui = "0.31" 11 | eframe = "0.31" 12 | petgraph = "0.8" 13 | 14 | # Wasm related dependencies 15 | getrandom = { version = "0.2", features = ["js"] } 16 | log = "0.4" 17 | instant = { version = "0.1", features = ["wasm-bindgen"] } 18 | 19 | 20 | # native: 21 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 22 | env_logger = "0.11" 23 | 24 | # web: 25 | [target.'cfg(target_arch = "wasm32")'.dependencies] 26 | wasm-bindgen-futures = "0.4" 27 | 28 | 29 | [profile.release] 30 | opt-level = 2 # fast and small wasm 31 | -------------------------------------------------------------------------------- /examples/wasm_custom_draw/README.md: -------------------------------------------------------------------------------- 1 | # Wasm Custom Draw 2 | 3 | Basic example which demonstrates the usage of `GraphView` widget in a wasm enviroment. 4 | 5 | ## prepare 6 | 7 | 1. Copy this example out to its own folder to use. 8 | 2. You will need to have Trunk installed to run this example: 9 | 10 | ```bash 11 | cargo install --locked trunk 12 | ``` 13 | 14 | 3. Install the required target with `rustup target add wasm32-unknown-unknown`. 15 | 16 | ## verify example with local run 17 | 18 | ```bash 19 | cargo run --release 20 | ``` 21 | 22 | ## run in web 23 | 24 | 1. Run 25 | 26 | ```bash 27 | trunk serve 28 | ``` 29 | 30 | to build and serve on `http://127.0.0.1:8080`. Trunk will rebuild automatically if you edit the project. 31 | 2. Open `http://127.0.0.1:8080/index.html` in a browser. 32 | -------------------------------------------------------------------------------- /examples/wasm_custom_draw/Trunk.toml: -------------------------------------------------------------------------------- 1 | [build] -------------------------------------------------------------------------------- /examples/wasm_custom_draw/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blitzar-tech/egui_graphs/789e47e4c92fb82bc82e401c5a3973b363f833a9/examples/wasm_custom_draw/assets/favicon.ico -------------------------------------------------------------------------------- /examples/wasm_custom_draw/assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasm", 3 | "short_name": "wasm", 4 | "icons": [ 5 | { 6 | "src": "./icon-256.png", 7 | "sizes": "256x256", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "./maskable_icon_x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png", 14 | "purpose": "any maskable" 15 | }, 16 | { 17 | "src": "./icon-1024.png", 18 | "sizes": "1024x1024", 19 | "type": "image/png" 20 | } 21 | ], 22 | "lang": "en-US", 23 | "id": "/index.html", 24 | "start_url": "./index.html", 25 | "display": "standalone", 26 | "background_color": "white", 27 | "theme_color": "white" 28 | } 29 | -------------------------------------------------------------------------------- /examples/wasm_custom_draw/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Wasm Example 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /examples/wasm_custom_draw/src/main.rs: -------------------------------------------------------------------------------- 1 | use eframe::{App, CreationContext}; 2 | use egui::Context; 3 | use egui_graphs::{DefaultEdgeShape, Graph, GraphView, SettingsInteraction, SettingsNavigation}; 4 | use node::NodeShapeAnimated; 5 | use petgraph::{ 6 | stable_graph::{DefaultIx, StableGraph}, 7 | Directed, 8 | }; 9 | 10 | mod node; 11 | 12 | pub struct CustomDrawApp { 13 | g: Graph<(), (), Directed, DefaultIx, NodeShapeAnimated>, 14 | } 15 | 16 | impl CustomDrawApp { 17 | fn new(_: &CreationContext<'_>) -> Self { 18 | let g = generate_graph(); 19 | Self { g: Graph::from(&g) } 20 | } 21 | } 22 | 23 | impl App for CustomDrawApp { 24 | fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { 25 | egui::CentralPanel::default().show(ctx, |ui| { 26 | ui.add( 27 | &mut GraphView::<_, _, _, _, NodeShapeAnimated, DefaultEdgeShape>::new(&mut self.g) 28 | .with_navigations( 29 | &SettingsNavigation::default() 30 | .with_fit_to_screen_enabled(false) 31 | .with_zoom_and_pan_enabled(true), 32 | ) 33 | .with_interactions( 34 | &SettingsInteraction::default() 35 | .with_dragging_enabled(true) 36 | .with_node_selection_enabled(true) 37 | .with_edge_selection_enabled(true), 38 | ), 39 | ); 40 | }); 41 | } 42 | } 43 | 44 | fn generate_graph() -> StableGraph<(), ()> { 45 | let mut g = StableGraph::new(); 46 | 47 | let a = g.add_node(()); 48 | let b = g.add_node(()); 49 | let c = g.add_node(()); 50 | 51 | g.add_edge(a, b, ()); 52 | g.add_edge(b, c, ()); 53 | g.add_edge(c, a, ()); 54 | 55 | g 56 | } 57 | 58 | // When compiling natively: 59 | #[cfg(not(target_arch = "wasm32"))] 60 | fn main() { 61 | let native_options = eframe::NativeOptions::default(); 62 | eframe::run_native( 63 | "egui_graphs_custom_draw_demo", 64 | native_options, 65 | Box::new(|cc| Ok(Box::new(CustomDrawApp::new(cc)))), 66 | ) 67 | .unwrap(); 68 | } 69 | 70 | // When compiling to web using trunk: 71 | #[cfg(target_arch = "wasm32")] 72 | fn main() { 73 | // Redirect `log` message to `console.log` and friends: 74 | eframe::WebLogger::init(log::LevelFilter::Debug).ok(); 75 | 76 | let web_options = eframe::WebOptions::default(); 77 | 78 | wasm_bindgen_futures::spawn_local(async { 79 | eframe::WebRunner::new() 80 | .start( 81 | "the_canvas_id", // hardcode it 82 | web_options, 83 | Box::new(|cc| Box::new(CustomDrawApp::new(cc))), 84 | ) 85 | .await 86 | .expect("failed to start eframe"); 87 | }); 88 | } 89 | -------------------------------------------------------------------------------- /examples/wasm_custom_draw/src/node.rs: -------------------------------------------------------------------------------- 1 | use instant::Instant; 2 | 3 | use egui::{ 4 | emath::Rot2, epaint::TextShape, Color32, FontFamily, FontId, Pos2, Rect, Shape, Stroke, Vec2, 5 | }; 6 | use egui_graphs::{DisplayNode, NodeProps}; 7 | use petgraph::{stable_graph::IndexType, EdgeType}; 8 | 9 | /// Rotates node whent the node is being dragged. 10 | #[derive(Clone)] 11 | pub struct NodeShapeAnimated { 12 | label: String, 13 | loc: Pos2, 14 | dragged: bool, 15 | 16 | angle_rad: f32, 17 | speed_per_second: f32, 18 | /// None means animation is not in progress 19 | last_time_update: Option, 20 | 21 | size: f32, 22 | } 23 | 24 | impl From> for NodeShapeAnimated { 25 | fn from(node_props: NodeProps) -> Self { 26 | Self { 27 | label: node_props.label.clone(), 28 | loc: node_props.location(), 29 | dragged: node_props.dragged, 30 | 31 | angle_rad: Default::default(), 32 | last_time_update: Default::default(), 33 | speed_per_second: 1., 34 | 35 | size: 30., 36 | } 37 | } 38 | } 39 | 40 | impl DisplayNode 41 | for NodeShapeAnimated 42 | { 43 | fn is_inside(&self, pos: Pos2) -> bool { 44 | let rotated_pos = rotate_point_around(self.loc, pos, -self.angle_rad); 45 | let rect = Rect::from_center_size(self.loc, Vec2::new(self.size, self.size)); 46 | 47 | rect.contains(rotated_pos) 48 | } 49 | 50 | fn closest_boundary_point(&self, dir: Vec2) -> Pos2 { 51 | let rotated_dir = rotate_vector(dir, -self.angle_rad); 52 | let intersection_point = find_intersection(self.loc, self.size, rotated_dir); 53 | rotate_point_around(self.loc, intersection_point, self.angle_rad) 54 | } 55 | 56 | fn shapes(&mut self, ctx: &egui_graphs::DrawContext) -> Vec { 57 | // lets draw a rect with label in the center for every node 58 | // which rotates when the node is dragged 59 | 60 | // find node center location on the screen coordinates 61 | let center = ctx.meta.canvas_to_screen_pos(self.loc); 62 | let size = ctx.meta.canvas_to_screen_size(self.size); 63 | let rect_default = Rect::from_center_size(center, Vec2::new(size, size)); 64 | let color = ctx.ctx.style().visuals.weak_text_color(); 65 | 66 | let diff = match self.dragged { 67 | true => { 68 | let now = Instant::now(); 69 | match self.last_time_update { 70 | Some(last_time) => { 71 | self.last_time_update = Some(now); 72 | let seconds_passed = now.duration_since(last_time); 73 | seconds_passed.as_secs_f32() * self.speed_per_second 74 | } 75 | None => { 76 | self.last_time_update = Some(now); 77 | 0. 78 | } 79 | } 80 | } 81 | false => { 82 | if self.last_time_update.is_some() { 83 | self.last_time_update = None; 84 | } 85 | 0. 86 | } 87 | }; 88 | 89 | if diff > 0. { 90 | let curr_angle = self.angle_rad + diff; 91 | let rot = Rot2::from_angle(curr_angle).normalized(); 92 | self.angle_rad = rot.angle(); 93 | }; 94 | 95 | let points = rect_to_points(rect_default) 96 | .into_iter() 97 | .map(|p| rotate_point_around(center, p, self.angle_rad)) 98 | .collect::>(); 99 | 100 | let shape_rect = Shape::convex_polygon(points, Color32::default(), Stroke::new(1., color)); 101 | 102 | // create label 103 | let color = ctx.ctx.style().visuals.text_color(); 104 | let galley = ctx.ctx.fonts(|f| { 105 | f.layout_no_wrap( 106 | self.label.clone(), 107 | FontId::new(ctx.meta.canvas_to_screen_size(10.), FontFamily::Monospace), 108 | color, 109 | ) 110 | }); 111 | 112 | // we need to offset label by half its size to place it in the center of the rect 113 | let offset = Vec2::new(-galley.size().x / 2., -galley.size().y / 2.); 114 | 115 | // create the shape and add it to the layers 116 | let shape_label = TextShape::new(center + offset, galley, color); 117 | 118 | vec![shape_rect, shape_label.into()] 119 | } 120 | 121 | fn update(&mut self, state: &NodeProps) { 122 | self.label = state.label.clone(); 123 | self.loc = state.location(); 124 | self.dragged = state.dragged; 125 | } 126 | } 127 | 128 | fn find_intersection(center: Pos2, size: f32, direction: Vec2) -> Pos2 { 129 | // Determine the intersection side based on the direction 130 | if direction.x.abs() > direction.y.abs() { 131 | // Intersects left or right side 132 | let x = if direction.x > 0.0 { 133 | center.x + size / 2.0 134 | } else { 135 | center.x - size / 2.0 136 | }; 137 | let y = center.y + direction.y / direction.x * (x - center.x); 138 | Pos2::new(x, y) 139 | } else { 140 | // Intersects top or bottom side 141 | let y = if direction.y > 0.0 { 142 | center.y + size / 2.0 143 | } else { 144 | center.y - size / 2.0 145 | }; 146 | let x = center.x + direction.x / direction.y * (y - center.y); 147 | Pos2::new(x, y) 148 | } 149 | } 150 | 151 | // Function to rotate a point around another point 152 | fn rotate_point_around(center: Pos2, point: Pos2, angle: f32) -> Pos2 { 153 | let sin_angle = angle.sin(); 154 | let cos_angle = angle.cos(); 155 | 156 | // Translate point back to origin 157 | let translated_point = point - center; 158 | 159 | // Rotate point 160 | let rotated_x = translated_point.x * cos_angle - translated_point.y * sin_angle; 161 | let rotated_y = translated_point.x * sin_angle + translated_point.y * cos_angle; 162 | 163 | // Translate point back 164 | Pos2::new(rotated_x, rotated_y) + center.to_vec2() 165 | } 166 | 167 | fn rect_to_points(rect: Rect) -> Vec { 168 | let top_left = rect.min; 169 | let bottom_right = rect.max; 170 | 171 | // Calculate the other two corners 172 | let top_right = Pos2::new(bottom_right.x, top_left.y); 173 | let bottom_left = Pos2::new(top_left.x, bottom_right.y); 174 | 175 | vec![top_left, top_right, bottom_right, bottom_left] 176 | } 177 | 178 | /// rotates vector by angle 179 | fn rotate_vector(vec: Vec2, angle: f32) -> Vec2 { 180 | let cos = angle.cos(); 181 | let sin = angle.sin(); 182 | Vec2::new(cos * vec.x - sin * vec.y, sin * vec.x + cos * vec.y) 183 | } 184 | 185 | #[cfg(test)] 186 | mod tests { 187 | use super::*; 188 | 189 | #[test] 190 | fn test_intersection_right_side() { 191 | let center = Pos2::new(0.0, 0.0); 192 | let size = 10.; 193 | let direction = Vec2::new(1.0, 0.0); // Directly to the right 194 | let expected = Pos2::new(5.0, 0.0); 195 | assert_eq!(find_intersection(center, size, direction), expected); 196 | } 197 | 198 | #[test] 199 | fn test_intersection_top_side() { 200 | let center = Pos2::new(0.0, 0.0); 201 | let size = 10.; 202 | let direction = Vec2::new(0.0, 1.0); // Directly upwards 203 | let expected = Pos2::new(0.0, 5.0); 204 | assert_eq!(find_intersection(center, size, direction), expected); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /examples/window/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "window" 3 | version = "0.1.0" 4 | authors = ["Dmitrii Samsonov "] 5 | license = "MIT" 6 | edition = "2021" 7 | 8 | [dependencies] 9 | egui_graphs = { path = "../.." } 10 | egui = "0.31" 11 | eframe = "0.31" 12 | petgraph = "0.8" -------------------------------------------------------------------------------- /examples/window/src/main.rs: -------------------------------------------------------------------------------- 1 | use eframe::{run_native, App, CreationContext}; 2 | use egui::{Context, Window}; 3 | use egui_graphs::{DefaultGraphView, Graph}; 4 | use petgraph::stable_graph::StableGraph; 5 | 6 | pub struct WindowApp { 7 | g: Graph, 8 | } 9 | 10 | impl WindowApp { 11 | fn new(_: &CreationContext<'_>) -> Self { 12 | let g = generate_graph(); 13 | Self { g: Graph::from(&g) } 14 | } 15 | } 16 | 17 | impl App for WindowApp { 18 | fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { 19 | Window::new("graph").show(ctx, |ui| { 20 | ui.add(&mut DefaultGraphView::new(&mut self.g)); 21 | }); 22 | } 23 | } 24 | 25 | fn generate_graph() -> StableGraph<(), ()> { 26 | let mut g = StableGraph::new(); 27 | 28 | let a = g.add_node(()); 29 | let b = g.add_node(()); 30 | let c = g.add_node(()); 31 | 32 | g.add_edge(a, b, ()); 33 | g.add_edge(b, c, ()); 34 | g.add_edge(c, a, ()); 35 | 36 | g 37 | } 38 | 39 | fn main() { 40 | let native_options = eframe::NativeOptions::default(); 41 | run_native( 42 | "egui_graphs_window_demo", 43 | native_options, 44 | Box::new(|cc| Ok(Box::new(WindowApp::new(cc)))), 45 | ) 46 | .unwrap(); 47 | } 48 | -------------------------------------------------------------------------------- /src/draw/displays.rs: -------------------------------------------------------------------------------- 1 | use egui::{Pos2, Shape, Vec2}; 2 | use petgraph::{stable_graph::IndexType, EdgeType}; 3 | 4 | use crate::{draw::drawer::DrawContext, elements::EdgeProps, Node, NodeProps}; 5 | 6 | pub trait DisplayNode: Clone + From> 7 | where 8 | N: Clone, 9 | E: Clone, 10 | Ty: EdgeType, 11 | Ix: IndexType, 12 | { 13 | /// Returns the closest point on the shape boundary in the direction of dir. 14 | /// 15 | /// * `dir` - direction pointing from the shape center to the required boundary point. 16 | /// 17 | /// Could be used to snap the edge ends to the node. 18 | fn closest_boundary_point(&self, dir: Vec2) -> Pos2; 19 | 20 | /// Draws shapes of the node. If the node is interacted these shapes will be used for drawing on foreground layer, otherwise on background layer. 21 | /// Has mutable reference to itself for possibility to change internal state for the visualizations where this is important. 22 | /// 23 | /// * `ctx` - contains [`egui::Context`] and graph metadata. 24 | /// 25 | /// Use `ctx.meta` to properly scale and translate the shape. 26 | /// Use `ctx.painter` to have low level access to egui painting process. 27 | fn shapes(&mut self, ctx: &DrawContext) -> Vec; 28 | 29 | /// Is called on every frame. Can be used for updating state of the implementation of [`DisplayNode`] 30 | fn update(&mut self, state: &NodeProps); 31 | 32 | /// Checks if the provided `pos` is inside the shape. 33 | /// 34 | /// * `pos` - position is in the canvas coordinates. 35 | /// 36 | /// Could be used to bind mouse events to the custom drawn nodes. 37 | fn is_inside(&self, pos: Pos2) -> bool; 38 | } 39 | 40 | pub trait DisplayEdge: Clone + From> 41 | where 42 | N: Clone, 43 | E: Clone, 44 | Ty: EdgeType, 45 | Ix: IndexType, 46 | D: DisplayNode, 47 | { 48 | /// Draws shapes of the edge. Uses [`DisplayNode`] implementation from node endpoints to get start and end coordinates using [`closest_boundary_point`](DisplayNode::closest_boundary_point). 49 | /// If the node is interacted these shapes will be used for drawing on foreground layer, otherwise on background layer. 50 | /// Has mutable reference to itself for possibility to change internal state for the visualizations where this is important. 51 | /// 52 | /// * `ctx` - contains [`egui::Context`] and graph metadata. 53 | /// * `start` and `end` - start and end points of the edge. 54 | /// 55 | /// Use `ctx.meta` to properly scale and translate the shape. 56 | /// Use `ctx.painter` to have low level access to egui painting process. 57 | fn shapes( 58 | &mut self, 59 | start: &Node, 60 | end: &Node, 61 | ctx: &DrawContext, 62 | ) -> Vec; 63 | 64 | /// Is called on every frame. Can be used for updating state of the implementation of [`DisplayNode`] 65 | fn update(&mut self, state: &EdgeProps); 66 | 67 | /// Checks if the provided `pos` is inside the shape. 68 | /// 69 | /// * `start` - start node of the edge. 70 | /// * `end` - end node of the edge. 71 | /// * `pos` - position is in the canvas coordinates. 72 | /// 73 | /// Could be used to bind mouse events to the custom drawn nodes. 74 | fn is_inside( 75 | &self, 76 | start: &Node, 77 | end: &Node, 78 | pos: Pos2, 79 | ) -> bool; 80 | } 81 | -------------------------------------------------------------------------------- /src/draw/displays_default/edge.rs: -------------------------------------------------------------------------------- 1 | use core::panic; 2 | 3 | use egui::{ 4 | epaint::{CubicBezierShape, TextShape}, 5 | Color32, FontFamily, FontId, Pos2, Shape, Stroke, Vec2, 6 | }; 7 | use petgraph::{stable_graph::IndexType, EdgeType}; 8 | 9 | use crate::{draw::DrawContext, elements::EdgeProps, node_size, DisplayEdge, DisplayNode, Node}; 10 | 11 | use super::edge_shape_builder::{EdgeShapeBuilder, TipProps}; 12 | 13 | #[derive(Clone, Debug)] 14 | pub struct DefaultEdgeShape { 15 | pub order: usize, 16 | pub selected: bool, 17 | 18 | pub width: f32, 19 | pub tip_size: f32, 20 | pub tip_angle: f32, 21 | pub curve_size: f32, 22 | pub loop_size: f32, 23 | pub label_text: String, 24 | } 25 | 26 | impl From> for DefaultEdgeShape { 27 | fn from(edge: EdgeProps) -> Self { 28 | Self { 29 | order: edge.order, 30 | selected: edge.selected, 31 | label_text: edge.label, 32 | 33 | width: 2., 34 | tip_size: 15., 35 | tip_angle: std::f32::consts::TAU / 30., 36 | curve_size: 20., 37 | loop_size: 3., 38 | } 39 | } 40 | } 41 | 42 | impl> 43 | DisplayEdge for DefaultEdgeShape 44 | { 45 | fn is_inside( 46 | &self, 47 | start: &Node, 48 | end: &Node, 49 | pos: egui::Pos2, 50 | ) -> bool { 51 | if start.id() == end.id() { 52 | return self.is_inside_loop(start, pos); 53 | } 54 | 55 | if self.order == 0 { 56 | return self.is_inside_line(start, end, pos); 57 | } 58 | 59 | self.is_inside_curve(start, end, pos) 60 | } 61 | 62 | #[allow(clippy::too_many_lines)] // TODO: refactor 63 | fn shapes( 64 | &mut self, 65 | start: &Node, 66 | end: &Node, 67 | ctx: &DrawContext, 68 | ) -> Vec { 69 | let mut res = vec![]; 70 | 71 | let label_visible = ctx.style.labels_always || self.selected; 72 | 73 | let style = if self.selected { 74 | ctx.ctx.style().visuals.widgets.active 75 | } else { 76 | ctx.ctx.style().visuals.widgets.inactive 77 | }; 78 | let color = style.fg_stroke.color; 79 | let stroke = Stroke::new(self.width, color); 80 | 81 | if start.id() == end.id() { 82 | // draw loop 83 | let size = node_size(start, Vec2::new(-1., 0.)); 84 | let mut line_looped_shapes = EdgeShapeBuilder::new(stroke) 85 | .looped(start.location(), size, self.loop_size, self.order) 86 | .with_scaler(ctx.meta) 87 | .build(); 88 | let line_looped_shape = line_looped_shapes.clone().pop().unwrap(); 89 | res.push(line_looped_shape); 90 | 91 | let Shape::CubicBezier(line_looped) = line_looped_shapes.pop().unwrap() else { 92 | panic!("invalid shape type") 93 | }; 94 | 95 | // TODO: export to func 96 | if label_visible { 97 | let galley = ctx.ctx.fonts(|f| { 98 | f.layout_no_wrap( 99 | self.label_text.clone(), 100 | FontId::new(ctx.meta.canvas_to_screen_size(size), FontFamily::Monospace), 101 | color, 102 | ) 103 | }); 104 | 105 | let flattened_curve = line_looped.flatten(None); 106 | let median = *flattened_curve.get(flattened_curve.len() / 2).unwrap(); 107 | 108 | let label_width = galley.rect.width(); 109 | let label_height = galley.rect.height(); 110 | let pos = Pos2::new(median.x - label_width / 2., median.y - label_height); 111 | 112 | let label_shape = TextShape::new(pos, galley, color); 113 | res.push(label_shape.into()); 114 | } 115 | return res; 116 | } 117 | 118 | let dir = (end.location() - start.location()).normalized(); 119 | let start_connector_point = start.display().closest_boundary_point(dir); 120 | let end_connector_point = end.display().closest_boundary_point(-dir); 121 | 122 | if self.order == 0 { 123 | // draw straight edge 124 | 125 | let mut builder = EdgeShapeBuilder::new(stroke) 126 | .straight((start_connector_point, end_connector_point)) 127 | .with_scaler(ctx.meta); 128 | 129 | let tip_props = TipProps { 130 | size: self.tip_size, 131 | angle: self.tip_angle, 132 | }; 133 | if ctx.is_directed { 134 | builder = builder.with_tip(&tip_props); 135 | } 136 | let straight_shapes = builder.build(); 137 | res.extend(straight_shapes); 138 | 139 | // TODO: export to func 140 | if label_visible { 141 | let size = (node_size(start, dir) + node_size(end, dir)) / 2.; 142 | let galley = ctx.ctx.fonts(|f| { 143 | f.layout_no_wrap( 144 | self.label_text.clone(), 145 | FontId::new(ctx.meta.canvas_to_screen_size(size), FontFamily::Monospace), 146 | color, 147 | ) 148 | }); 149 | 150 | let dist = end_connector_point - start_connector_point; 151 | let center = ctx 152 | .meta 153 | .canvas_to_screen_pos(start_connector_point + dist / 2.); 154 | let label_width = galley.rect.width(); 155 | let label_height = galley.rect.height(); 156 | let pos = Pos2::new(center.x - label_width / 2., center.y - label_height); 157 | 158 | let label_shape = TextShape::new(pos, galley, color); 159 | res.push(label_shape.into()); 160 | } 161 | 162 | return res; 163 | } 164 | 165 | let mut builder = EdgeShapeBuilder::new(stroke) 166 | .curved( 167 | (start_connector_point, end_connector_point), 168 | self.curve_size, 169 | self.order, 170 | ) 171 | .with_scaler(ctx.meta); 172 | 173 | let tip_props = TipProps { 174 | size: self.tip_size, 175 | angle: self.tip_angle, 176 | }; 177 | if ctx.is_directed { 178 | builder = builder.with_tip(&tip_props); 179 | } 180 | let curved_shapes = builder.build(); 181 | let Some(Shape::CubicBezier(line_curved)) = curved_shapes.first() else { 182 | panic!("invalid shape type") 183 | }; 184 | res.extend(curved_shapes.clone()); 185 | 186 | if label_visible { 187 | let size = (node_size(start, dir) + node_size(end, dir)) / 2.; 188 | let galley = ctx.ctx.fonts(|f| { 189 | f.layout_no_wrap( 190 | self.label_text.clone(), 191 | FontId::new(ctx.meta.canvas_to_screen_size(size), FontFamily::Monospace), 192 | color, 193 | ) 194 | }); 195 | 196 | let flattened_curve = line_curved.flatten(None); 197 | let median = *flattened_curve.get(flattened_curve.len() / 2).unwrap(); 198 | 199 | let label_width = galley.rect.width(); 200 | let label_height = galley.rect.height(); 201 | let pos = Pos2::new(median.x - label_width / 2., median.y - label_height); 202 | 203 | let label_shape = TextShape::new(pos, galley, color); 204 | res.push(label_shape.into()); 205 | } 206 | 207 | res 208 | } 209 | 210 | fn update(&mut self, state: &EdgeProps) { 211 | self.order = state.order; 212 | self.selected = state.selected; 213 | self.label_text = state.label.to_string(); 214 | } 215 | } 216 | 217 | impl DefaultEdgeShape { 218 | fn is_inside_loop< 219 | E: Clone, 220 | N: Clone, 221 | Ix: IndexType, 222 | Ty: EdgeType, 223 | D: DisplayNode, 224 | >( 225 | &self, 226 | node: &Node, 227 | pos: Pos2, 228 | ) -> bool { 229 | let node_size = node_size(node, Vec2::new(-1., 0.)); 230 | 231 | let shape = EdgeShapeBuilder::new(Stroke::new(self.width, Color32::default())) 232 | .looped(node.location(), node_size, self.loop_size, self.order) 233 | .build(); 234 | 235 | match shape.first() { 236 | Some(Shape::CubicBezier(cubic)) => is_point_on_curve(pos, cubic, self.width), 237 | _ => panic!("invalid shape type"), 238 | } 239 | } 240 | 241 | fn is_inside_line< 242 | E: Clone, 243 | N: Clone, 244 | Ix: IndexType, 245 | Ty: EdgeType, 246 | D: DisplayNode, 247 | >( 248 | &self, 249 | start: &Node, 250 | end: &Node, 251 | pos: Pos2, 252 | ) -> bool { 253 | distance_segment_to_point(start.location(), end.location(), pos) <= self.width 254 | } 255 | 256 | fn is_inside_curve< 257 | N: Clone, 258 | E: Clone, 259 | Ty: EdgeType, 260 | Ix: IndexType, 261 | D: DisplayNode, 262 | >( 263 | &self, 264 | node_start: &Node, 265 | node_end: &Node, 266 | pos: Pos2, 267 | ) -> bool { 268 | let dir = (node_end.location() - node_start.location()).normalized(); 269 | let start = node_start.display().closest_boundary_point(dir); 270 | let end = node_end.display().closest_boundary_point(-dir); 271 | 272 | let stroke = Stroke::new(self.width, Color32::default()); 273 | let curved_shapes = EdgeShapeBuilder::new(stroke) 274 | .curved((start, end), self.curve_size, self.order) 275 | .build(); 276 | 277 | let curved_shape = match curved_shapes.first() { 278 | Some(Shape::CubicBezier(curve)) => curve.clone(), 279 | _ => panic!("invalid shape type"), 280 | }; 281 | is_point_on_curve(pos, &curved_shape, self.width) 282 | } 283 | } 284 | 285 | /// Returns the distance from line segment `a``b` to point `c`. 286 | /// Adapted from 287 | fn distance_segment_to_point(a: Pos2, b: Pos2, point: Pos2) -> f32 { 288 | let ac = point - a; 289 | let ab = b - a; 290 | 291 | let d = a + proj(ac, ab); 292 | 293 | let ad = d - a; 294 | 295 | let k = if ab.x.abs() > ab.y.abs() { 296 | ad.x / ab.x 297 | } else { 298 | ad.y / ab.y 299 | }; 300 | 301 | if k <= 0.0 { 302 | return hypot2(point.to_vec2(), a.to_vec2()).sqrt(); 303 | } else if k >= 1.0 { 304 | return hypot2(point.to_vec2(), b.to_vec2()).sqrt(); 305 | } 306 | 307 | hypot2(point.to_vec2(), d.to_vec2()).sqrt() 308 | } 309 | 310 | /// Calculates the square of the Euclidean distance between vectors `a` and `b`. 311 | fn hypot2(a: Vec2, b: Vec2) -> f32 { 312 | (a - b).dot(a - b) 313 | } 314 | 315 | /// Calculates the projection of vector `a` onto vector `b`. 316 | fn proj(a: Vec2, b: Vec2) -> Vec2 { 317 | let k = a.dot(b) / b.dot(b); 318 | Vec2::new(k * b.x, k * b.y) 319 | } 320 | 321 | fn is_point_on_curve(point: Pos2, curve: &CubicBezierShape, tolerance: f32) -> bool { 322 | for p in curve.flatten(None) { 323 | if p.distance(point) < tolerance { 324 | return true; 325 | } 326 | } 327 | false 328 | } 329 | 330 | #[cfg(test)] 331 | mod tests { 332 | use super::*; 333 | 334 | #[test] 335 | fn test_distance_segment_to_point() { 336 | let segment_1 = Pos2::new(2.0, 2.0); 337 | let segment_2 = Pos2::new(2.0, 5.0); 338 | let point = Pos2::new(4.0, 3.0); 339 | assert_eq!(distance_segment_to_point(segment_1, segment_2, point), 2.0); 340 | } 341 | 342 | #[test] 343 | fn test_distance_segment_to_point_on_segment() { 344 | let segment_1 = Pos2::new(1.0, 2.0); 345 | let segment_2 = Pos2::new(1.0, 5.0); 346 | let point = Pos2::new(1.0, 3.0); 347 | assert_eq!(distance_segment_to_point(segment_1, segment_2, point), 0.0); 348 | } 349 | 350 | #[test] 351 | fn test_hypot2() { 352 | let a = Vec2::new(0.0, 1.0); 353 | let b = Vec2::new(0.0, 5.0); 354 | assert_eq!(hypot2(a, b), 16.0); 355 | } 356 | 357 | #[test] 358 | fn test_hypot2_no_distance() { 359 | let a = Vec2::new(0.0, 1.0); 360 | assert_eq!(hypot2(a, a), 0.0); 361 | } 362 | 363 | #[test] 364 | fn test_proj() { 365 | let a = Vec2::new(5.0, 8.0); 366 | let b = Vec2::new(10.0, 0.0); 367 | let result = proj(a, b); 368 | assert_eq!(result.x, 5.0); 369 | assert_eq!(result.y, 0.0); 370 | } 371 | 372 | #[test] 373 | fn test_proj_orthogonal() { 374 | let a = Vec2::new(5.0, 0.0); 375 | let b = Vec2::new(0.0, 5.0); 376 | let result = proj(a, b); 377 | assert_eq!(result.x, 0.0); 378 | assert_eq!(result.y, 0.0); 379 | } 380 | 381 | #[test] 382 | fn test_proj_same_vector() { 383 | let a = Vec2::new(5.3, 4.9); 384 | assert_eq!(proj(a, a), a); 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /src/draw/displays_default/edge_shape_builder.rs: -------------------------------------------------------------------------------- 1 | use std::f32::consts::PI; 2 | 3 | use egui::{epaint::CubicBezierShape, Color32, Pos2, Shape, Stroke, Vec2}; 4 | 5 | use crate::Metadata; 6 | 7 | enum EdgeShapeProps { 8 | Straight { 9 | bounds: (Pos2, Pos2), 10 | }, 11 | Curved { 12 | bounds: (Pos2, Pos2), 13 | curve_size: f32, 14 | order: usize, 15 | }, 16 | Looped { 17 | node_center: Pos2, 18 | node_size: f32, 19 | loop_size: f32, 20 | order: usize, 21 | }, 22 | } 23 | 24 | impl Default for EdgeShapeProps { 25 | fn default() -> Self { 26 | Self::Straight { 27 | bounds: (Pos2::default(), Pos2::default()), 28 | } 29 | } 30 | } 31 | 32 | #[derive(Default)] 33 | pub struct TipProps { 34 | pub size: f32, 35 | pub angle: f32, 36 | } 37 | 38 | #[derive(Default)] 39 | pub struct EdgeShapeBuilder<'a> { 40 | shape_props: EdgeShapeProps, 41 | tip: Option<&'a TipProps>, 42 | stroke: Stroke, 43 | scaler: Option<&'a Metadata>, 44 | } 45 | 46 | impl<'a> EdgeShapeBuilder<'a> { 47 | pub fn new(stroke: Stroke) -> Self { 48 | Self { 49 | stroke, 50 | ..Default::default() 51 | } 52 | } 53 | 54 | pub fn straight(mut self, bounds: (Pos2, Pos2)) -> Self { 55 | self.shape_props = EdgeShapeProps::Straight { bounds }; 56 | 57 | self 58 | } 59 | 60 | pub fn curved(mut self, bounds: (Pos2, Pos2), curve_size: f32, order: usize) -> Self { 61 | self.shape_props = EdgeShapeProps::Curved { 62 | bounds, 63 | curve_size, 64 | order, 65 | }; 66 | 67 | self 68 | } 69 | 70 | pub fn looped( 71 | mut self, 72 | node_center: Pos2, 73 | node_size: f32, 74 | loop_size: f32, 75 | order: usize, 76 | ) -> Self { 77 | self.shape_props = EdgeShapeProps::Looped { 78 | node_center, 79 | node_size, 80 | loop_size, 81 | order, 82 | }; 83 | 84 | self 85 | } 86 | 87 | pub fn with_scaler(mut self, scaler: &'a Metadata) -> Self { 88 | self.scaler = Some(scaler); 89 | 90 | self 91 | } 92 | 93 | pub fn with_tip(mut self, tip_props: &'a TipProps) -> Self { 94 | self.tip = Some(tip_props); 95 | 96 | self 97 | } 98 | 99 | pub fn shape_straight(&self, bounds: (Pos2, Pos2)) -> Vec { 100 | let mut res = vec![]; 101 | 102 | let (start, end) = bounds; 103 | let mut stroke = self.stroke; 104 | 105 | let mut points_line = vec![start, end]; 106 | let mut points_tip = match self.tip { 107 | Some(tip_props) => { 108 | let tip_dir = (end - start).normalized(); 109 | 110 | let arrow_tip_dir_1 = rotate_vector(tip_dir, tip_props.angle) * tip_props.size; 111 | let arrow_tip_dir_2 = rotate_vector(tip_dir, -tip_props.angle) * tip_props.size; 112 | 113 | let tip_start_1 = end - arrow_tip_dir_1; 114 | let tip_start_2 = end - arrow_tip_dir_2; 115 | 116 | // replace end of an edge with start of tip 117 | *points_line.get_mut(1).unwrap() = end - tip_props.size * tip_dir; 118 | 119 | vec![end, tip_start_1, tip_start_2] 120 | } 121 | None => vec![], 122 | }; 123 | 124 | if let Some(scaler) = self.scaler { 125 | stroke.width = scaler.canvas_to_screen_size(stroke.width); 126 | points_line = points_line 127 | .iter() 128 | .map(|p| scaler.canvas_to_screen_pos(*p)) 129 | .collect(); 130 | points_tip = points_tip 131 | .iter() 132 | .map(|p| scaler.canvas_to_screen_pos(*p)) 133 | .collect(); 134 | } 135 | 136 | res.push(Shape::line_segment( 137 | [points_line[0], points_line[1]], 138 | stroke, 139 | )); 140 | if !points_tip.is_empty() { 141 | res.push(Shape::convex_polygon( 142 | points_tip, 143 | stroke.color, 144 | Stroke::default(), 145 | )); 146 | } 147 | 148 | res 149 | } 150 | 151 | fn shape_looped( 152 | &self, 153 | node_center: Pos2, 154 | node_size: f32, 155 | loop_size: f32, 156 | param: f32, 157 | ) -> Vec { 158 | let mut res = vec![]; 159 | 160 | let mut stroke = self.stroke; 161 | let center_horizon_angle = PI / 4.; 162 | let y_intersect = node_center.y - node_size * center_horizon_angle.sin(); 163 | 164 | let mut edge_start = Pos2::new( 165 | node_center.x - node_size * center_horizon_angle.cos(), 166 | y_intersect, 167 | ); 168 | let mut edge_end = Pos2::new( 169 | node_center.x + node_size * center_horizon_angle.cos(), 170 | y_intersect, 171 | ); 172 | 173 | let loop_size = node_size * (loop_size + param); 174 | 175 | let mut control_point1 = Pos2::new(node_center.x + loop_size, node_center.y - loop_size); 176 | let mut control_point2 = Pos2::new(node_center.x - loop_size, node_center.y - loop_size); 177 | 178 | if let Some(scaler) = self.scaler { 179 | stroke.width = scaler.canvas_to_screen_size(stroke.width); 180 | edge_end = scaler.canvas_to_screen_pos(edge_end); 181 | control_point1 = scaler.canvas_to_screen_pos(control_point1); 182 | control_point2 = scaler.canvas_to_screen_pos(control_point2); 183 | edge_start = scaler.canvas_to_screen_pos(edge_start); 184 | } 185 | 186 | res.push( 187 | CubicBezierShape::from_points_stroke( 188 | [edge_end, control_point1, control_point2, edge_start], 189 | false, 190 | Color32::default(), 191 | stroke, 192 | ) 193 | .into(), 194 | ); 195 | res 196 | } 197 | 198 | fn shape_curved(&self, bounds: (Pos2, Pos2), curve_size: f32, param: f32) -> Vec { 199 | let mut res = vec![]; 200 | let (start, end) = bounds; 201 | let mut stroke = self.stroke; 202 | 203 | let dist = end - start; 204 | let dir = dist.normalized(); 205 | let dir_p = Vec2::new(-dir.y, dir.x); 206 | let center_point = (start + end.to_vec2()) / 2.0; 207 | let height = dir_p * curve_size * param; 208 | let cp = center_point + height; 209 | 210 | let cp_start = cp - dir * curve_size / (param * dist * 0.5); 211 | let cp_end = cp + dir * curve_size / (param * dist * 0.5); 212 | 213 | let mut points_curve = vec![start, cp_start, cp_end, end]; 214 | 215 | let mut points_tip = match self.tip { 216 | Some(tip_props) => { 217 | let tip_dir = (end - cp).normalized(); 218 | 219 | let arrow_tip_dir_1 = rotate_vector(tip_dir, tip_props.angle) * tip_props.size; 220 | let arrow_tip_dir_2 = rotate_vector(tip_dir, -tip_props.angle) * tip_props.size; 221 | 222 | let tip_start_1 = end - arrow_tip_dir_1; 223 | let tip_start_2 = end - arrow_tip_dir_2; 224 | 225 | // replace end of an edge with start of tip 226 | *points_curve.get_mut(3).unwrap() = end - tip_props.size * tip_dir; 227 | 228 | vec![end, tip_start_1, tip_start_2] 229 | } 230 | None => vec![], 231 | }; 232 | 233 | if let Some(scaler) = self.scaler { 234 | stroke.width = scaler.canvas_to_screen_size(stroke.width); 235 | points_curve = points_curve 236 | .iter() 237 | .map(|p| scaler.canvas_to_screen_pos(*p)) 238 | .collect(); 239 | points_tip = points_tip 240 | .iter() 241 | .map(|p| scaler.canvas_to_screen_pos(*p)) 242 | .collect(); 243 | } 244 | 245 | res.push( 246 | CubicBezierShape::from_points_stroke( 247 | [ 248 | points_curve[0], 249 | points_curve[1], 250 | points_curve[2], 251 | points_curve[3], 252 | ], 253 | false, 254 | Color32::default(), 255 | stroke, 256 | ) 257 | .into(), 258 | ); 259 | if !points_tip.is_empty() { 260 | res.push(Shape::convex_polygon( 261 | points_tip, 262 | stroke.color, 263 | Stroke::default(), 264 | )); 265 | } 266 | 267 | res 268 | } 269 | 270 | pub fn build(&self) -> Vec { 271 | match self.shape_props { 272 | EdgeShapeProps::Straight { bounds } => self.shape_straight(bounds), 273 | EdgeShapeProps::Looped { 274 | node_center, 275 | node_size, 276 | loop_size, 277 | order, 278 | } => { 279 | let param: f32 = order as f32; 280 | self.shape_looped(node_center, node_size, loop_size, param) 281 | } 282 | EdgeShapeProps::Curved { 283 | bounds, 284 | curve_size, 285 | order, 286 | } => { 287 | let param: f32 = order as f32; 288 | self.shape_curved(bounds, curve_size, param) 289 | } 290 | } 291 | } 292 | } 293 | 294 | /// rotates vector by angle 295 | fn rotate_vector(vec: Vec2, angle: f32) -> Vec2 { 296 | let cos = angle.cos(); 297 | let sin = angle.sin(); 298 | Vec2::new(cos * vec.x - sin * vec.y, sin * vec.x + cos * vec.y) 299 | } 300 | -------------------------------------------------------------------------------- /src/draw/displays_default/mod.rs: -------------------------------------------------------------------------------- 1 | mod edge; 2 | mod edge_shape_builder; 3 | mod node; 4 | 5 | pub use edge::DefaultEdgeShape; 6 | pub use node::DefaultNodeShape; 7 | -------------------------------------------------------------------------------- /src/draw/displays_default/node.rs: -------------------------------------------------------------------------------- 1 | use egui::{ 2 | epaint::{CircleShape, TextShape}, 3 | Color32, FontFamily, FontId, Pos2, Shape, Stroke, Vec2, 4 | }; 5 | use petgraph::{stable_graph::IndexType, EdgeType}; 6 | 7 | use crate::{draw::drawer::DrawContext, DisplayNode, NodeProps}; 8 | 9 | /// This is the default node shape which is used to display nodes in the graph. 10 | /// 11 | /// You can use this implementation as an example for implementing your own custom node shapes. 12 | #[derive(Clone, Debug)] 13 | pub struct DefaultNodeShape { 14 | pub pos: Pos2, 15 | 16 | pub selected: bool, 17 | pub dragged: bool, 18 | pub color: Option, 19 | 20 | pub label_text: String, 21 | 22 | /// Shape dependent property 23 | pub radius: f32, 24 | } 25 | 26 | impl From> for DefaultNodeShape { 27 | fn from(node_props: NodeProps) -> Self { 28 | DefaultNodeShape { 29 | pos: node_props.location(), 30 | selected: node_props.selected, 31 | dragged: node_props.dragged, 32 | label_text: node_props.label.to_string(), 33 | color: node_props.color(), 34 | 35 | radius: 5.0, 36 | } 37 | } 38 | } 39 | 40 | impl DisplayNode 41 | for DefaultNodeShape 42 | { 43 | fn is_inside(&self, pos: Pos2) -> bool { 44 | is_inside_circle(self.pos, self.radius, pos) 45 | } 46 | 47 | fn closest_boundary_point(&self, dir: Vec2) -> Pos2 { 48 | closest_point_on_circle(self.pos, self.radius, dir) 49 | } 50 | 51 | fn shapes(&mut self, ctx: &DrawContext) -> Vec { 52 | let mut res = Vec::with_capacity(2); 53 | 54 | let is_interacted = self.selected || self.dragged; 55 | 56 | let style = if is_interacted { 57 | ctx.ctx.style().visuals.widgets.active 58 | } else { 59 | ctx.ctx.style().visuals.widgets.inactive 60 | }; 61 | 62 | let color = if let Some(c) = self.color { 63 | c 64 | } else { 65 | style.fg_stroke.color 66 | }; 67 | 68 | let circle_center = ctx.meta.canvas_to_screen_pos(self.pos); 69 | let circle_radius = ctx.meta.canvas_to_screen_size(self.radius); 70 | let circle_shape = CircleShape { 71 | center: circle_center, 72 | radius: circle_radius, 73 | fill: color, 74 | stroke: Stroke::default(), 75 | }; 76 | res.push(circle_shape.into()); 77 | 78 | let label_visible = ctx.style.labels_always || self.selected || self.dragged; 79 | if !label_visible { 80 | return res; 81 | } 82 | 83 | let galley = ctx.ctx.fonts(|f| { 84 | f.layout_no_wrap( 85 | self.label_text.clone(), 86 | FontId::new(circle_radius, FontFamily::Monospace), 87 | color, 88 | ) 89 | }); 90 | 91 | // display label centered over the circle 92 | let label_pos = Pos2::new( 93 | circle_center.x - galley.size().x / 2., 94 | circle_center.y - circle_radius * 2., 95 | ); 96 | 97 | let label_shape = TextShape::new(label_pos, galley, color); 98 | res.push(label_shape.into()); 99 | 100 | res 101 | } 102 | 103 | fn update(&mut self, state: &NodeProps) { 104 | self.pos = state.location(); 105 | self.selected = state.selected; 106 | self.dragged = state.dragged; 107 | self.label_text = state.label.to_string(); 108 | self.color = state.color(); 109 | } 110 | } 111 | 112 | fn closest_point_on_circle(center: Pos2, radius: f32, dir: Vec2) -> Pos2 { 113 | center + dir.normalized() * radius 114 | } 115 | 116 | fn is_inside_circle(center: Pos2, radius: f32, pos: Pos2) -> bool { 117 | let dir = pos - center; 118 | dir.length() <= radius 119 | } 120 | 121 | #[cfg(test)] 122 | mod test { 123 | use super::*; 124 | use egui::Pos2; 125 | 126 | #[test] 127 | fn test_closest_point_on_circle() { 128 | assert_eq!( 129 | closest_point_on_circle(Pos2::new(0.0, 0.0), 10.0, Vec2::new(5.0, 0.0)), 130 | Pos2::new(10.0, 0.0) 131 | ); 132 | assert_eq!( 133 | closest_point_on_circle(Pos2::new(0.0, 0.0), 10.0, Vec2::new(15.0, 0.0)), 134 | Pos2::new(10.0, 0.0) 135 | ); 136 | assert_eq!( 137 | closest_point_on_circle(Pos2::new(0.0, 0.0), 10.0, Vec2::new(0.0, 10.0)), 138 | Pos2::new(0.0, 10.0) 139 | ); 140 | } 141 | 142 | #[test] 143 | fn test_is_inside_circle() { 144 | assert!(is_inside_circle( 145 | Pos2::new(0.0, 0.0), 146 | 10.0, 147 | Pos2::new(5.0, 0.0) 148 | )); 149 | assert!(!is_inside_circle( 150 | Pos2::new(0.0, 0.0), 151 | 10.0, 152 | Pos2::new(15.0, 0.0) 153 | )); 154 | assert!(is_inside_circle( 155 | Pos2::new(0.0, 0.0), 156 | 10.0, 157 | Pos2::new(0.0, 10.0) 158 | )); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/draw/drawer.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use egui::{Context, Painter, Shape}; 4 | use petgraph::graph::IndexType; 5 | use petgraph::EdgeType; 6 | 7 | use crate::{ 8 | layouts::{Layout, LayoutState}, 9 | settings::SettingsStyle, 10 | Graph, Metadata, 11 | }; 12 | 13 | use super::{DisplayEdge, DisplayNode}; 14 | 15 | /// Contains all the data about current widget state which is needed for custom drawing functions. 16 | pub struct DrawContext<'a> { 17 | pub ctx: &'a Context, 18 | pub painter: &'a Painter, 19 | pub style: &'a SettingsStyle, 20 | pub is_directed: bool, 21 | pub meta: &'a Metadata, 22 | } 23 | 24 | pub struct Drawer<'a, N, E, Ty, Ix, Nd, Ed, S, L> 25 | where 26 | N: Clone, 27 | E: Clone, 28 | Ty: EdgeType, 29 | Ix: IndexType, 30 | Nd: DisplayNode, 31 | Ed: DisplayEdge, 32 | S: LayoutState, 33 | L: Layout, 34 | { 35 | ctx: &'a DrawContext<'a>, 36 | g: &'a mut Graph, 37 | delayed: Vec, 38 | 39 | _marker: PhantomData<(Nd, Ed, L, S)>, 40 | } 41 | 42 | impl<'a, N, E, Ty, Ix, Nd, Ed, S, L> Drawer<'a, N, E, Ty, Ix, Nd, Ed, S, L> 43 | where 44 | N: Clone, 45 | E: Clone, 46 | Ty: EdgeType, 47 | Ix: IndexType, 48 | Nd: DisplayNode, 49 | Ed: DisplayEdge, 50 | S: LayoutState, 51 | L: Layout, 52 | { 53 | pub fn new(g: &'a mut Graph, ctx: &'a DrawContext<'a>) -> Self { 54 | Drawer { 55 | ctx, 56 | g, 57 | delayed: Vec::new(), 58 | _marker: PhantomData, 59 | } 60 | } 61 | 62 | pub fn draw(mut self) { 63 | self.draw_edges(); 64 | self.draw_nodes(); 65 | self.draw_postponed(); 66 | } 67 | 68 | fn draw_postponed(&mut self) { 69 | self.delayed.iter().for_each(|s| { 70 | self.ctx.painter.add(s.clone()); 71 | }); 72 | } 73 | 74 | fn draw_nodes(&mut self) { 75 | self.g 76 | .g 77 | .node_indices() 78 | .collect::>() 79 | .into_iter() 80 | .for_each(|idx| { 81 | let n = self.g.node_mut(idx).unwrap(); 82 | let props = n.props().clone(); 83 | 84 | let display = n.display_mut(); 85 | display.update(&props); 86 | let shapes = display.shapes(self.ctx); 87 | 88 | if n.selected() || n.dragged() { 89 | for s in shapes { 90 | self.delayed.push(s); 91 | } 92 | } else { 93 | for s in shapes { 94 | self.ctx.painter.add(s); 95 | } 96 | } 97 | }); 98 | } 99 | 100 | fn draw_edges(&mut self) { 101 | self.g 102 | .g 103 | .edge_indices() 104 | .collect::>() 105 | .into_iter() 106 | .for_each(|idx| { 107 | let (idx_start, idx_end) = self.g.edge_endpoints(idx).unwrap(); 108 | 109 | // FIXME: too costly to clone nodes for every edge 110 | let start = self.g.node(idx_start).cloned().unwrap(); 111 | let end = self.g.node(idx_end).cloned().unwrap(); 112 | 113 | let e = self.g.edge_mut(idx).unwrap(); 114 | let props = e.props().clone(); 115 | 116 | let display = e.display_mut(); 117 | display.update(&props); 118 | let shapes = display.shapes(&start, &end, self.ctx); 119 | 120 | if e.selected() { 121 | for s in shapes { 122 | self.delayed.push(s); 123 | } 124 | } else { 125 | for s in shapes { 126 | self.ctx.painter.add(s); 127 | } 128 | } 129 | }); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/draw/mod.rs: -------------------------------------------------------------------------------- 1 | mod displays; 2 | mod displays_default; 3 | mod drawer; 4 | 5 | pub use displays::{DisplayEdge, DisplayNode}; 6 | pub use displays_default::DefaultEdgeShape; 7 | pub use displays_default::DefaultNodeShape; 8 | pub use drawer::{DrawContext, Drawer}; 9 | -------------------------------------------------------------------------------- /src/elements/edge.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use petgraph::{ 4 | stable_graph::{DefaultIx, EdgeIndex, IndexType}, 5 | Directed, EdgeType, 6 | }; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use crate::{DefaultEdgeShape, DefaultNodeShape, DisplayEdge, DisplayNode}; 10 | 11 | /// Stores properties of an [Edge] 12 | #[derive(Clone, Debug, Serialize, Deserialize)] 13 | pub struct EdgeProps { 14 | pub payload: E, 15 | pub order: usize, 16 | pub selected: bool, 17 | pub label: String, 18 | } 19 | 20 | /// Stores properties of an edge that can be changed. Used to apply changes to the graph. 21 | #[derive(Clone, Debug, Serialize, Deserialize)] 22 | pub struct Edge< 23 | N: Clone, 24 | E: Clone, 25 | Ty: EdgeType = Directed, 26 | Ix: IndexType = DefaultIx, 27 | Dn: DisplayNode = DefaultNodeShape, 28 | D: DisplayEdge = DefaultEdgeShape, 29 | > { 30 | id: Option>, 31 | 32 | display: D, 33 | 34 | props: EdgeProps, 35 | _marker: PhantomData<(N, Ty, Dn)>, 36 | } 37 | 38 | impl< 39 | N: Clone, 40 | E: Clone, 41 | Ty: EdgeType, 42 | Ix: IndexType, 43 | Dn: DisplayNode, 44 | D: DisplayEdge, 45 | > Edge 46 | { 47 | pub fn new(payload: E) -> Self { 48 | let props = EdgeProps { 49 | payload, 50 | 51 | order: usize::default(), 52 | selected: bool::default(), 53 | label: String::default(), 54 | }; 55 | 56 | let display = D::from(props.clone()); 57 | Self { 58 | props, 59 | display, 60 | 61 | id: Option::default(), 62 | _marker: PhantomData, 63 | } 64 | } 65 | 66 | pub fn props(&self) -> &EdgeProps { 67 | &self.props 68 | } 69 | 70 | pub fn display(&self) -> &D { 71 | &self.display 72 | } 73 | 74 | pub fn display_mut(&mut self) -> &mut D { 75 | &mut self.display 76 | } 77 | 78 | #[allow(clippy::missing_panics_doc)] // TODO: Add panic message 79 | pub fn id(&self) -> EdgeIndex { 80 | self.id.unwrap() 81 | } 82 | 83 | pub(crate) fn set_id(&mut self, id: EdgeIndex) { 84 | self.id = Some(id); 85 | } 86 | 87 | pub fn order(&self) -> usize { 88 | self.props.order 89 | } 90 | 91 | pub(crate) fn set_order(&mut self, order: usize) { 92 | self.props.order = order; 93 | } 94 | 95 | pub fn payload(&self) -> &E { 96 | &self.props.payload 97 | } 98 | 99 | pub fn payload_mut(&mut self) -> &mut E { 100 | &mut self.props.payload 101 | } 102 | 103 | pub fn set_selected(&mut self, selected: bool) { 104 | self.props.selected = selected; 105 | } 106 | 107 | pub fn selected(&self) -> bool { 108 | self.props.selected 109 | } 110 | 111 | pub fn set_label(&mut self, label: String) { 112 | self.props.label = label; 113 | } 114 | 115 | pub fn label(&self) -> String { 116 | self.props.label.clone() 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/elements/mod.rs: -------------------------------------------------------------------------------- 1 | mod edge; 2 | mod node; 3 | 4 | pub use edge::{Edge, EdgeProps}; 5 | pub use node::{Node, NodeProps}; 6 | -------------------------------------------------------------------------------- /src/elements/node.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::marker::PhantomData; 3 | 4 | use egui::{Color32, Pos2}; 5 | use petgraph::{ 6 | stable_graph::{DefaultIx, IndexType, NodeIndex}, 7 | Directed, EdgeType, 8 | }; 9 | use serde::{Deserialize, Serialize}; 10 | 11 | use crate::{DefaultNodeShape, DisplayNode}; 12 | 13 | /// Stores properties of a [Node] 14 | #[derive(Clone, Debug, Serialize, Deserialize)] 15 | pub struct NodeProps 16 | where 17 | N: Clone, 18 | { 19 | pub payload: N, 20 | pub label: String, 21 | pub selected: bool, 22 | pub dragged: bool, 23 | 24 | color: Option, 25 | location: Pos2, 26 | } 27 | 28 | impl NodeProps 29 | where 30 | N: Clone, 31 | { 32 | pub fn location(&self) -> Pos2 { 33 | self.location 34 | } 35 | 36 | pub fn color(&self) -> Option { 37 | self.color 38 | } 39 | } 40 | 41 | #[derive(Serialize, Deserialize)] 42 | pub struct Node 43 | where 44 | N: Clone, 45 | E: Clone, 46 | Ty: EdgeType, 47 | Ix: IndexType, 48 | D: DisplayNode, 49 | { 50 | id: Option>, 51 | 52 | props: NodeProps, 53 | display: D, 54 | 55 | _marker: PhantomData<(E, Ty)>, 56 | } 57 | 58 | #[allow(clippy::missing_fields_in_debug)] // TODO: add all fields or remove this and fix all warnings 59 | impl Debug for Node 60 | where 61 | N: Clone, 62 | E: Clone, 63 | Ty: EdgeType, 64 | Ix: IndexType, 65 | D: DisplayNode, 66 | { 67 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 68 | f.debug_struct("Node").field("id", &self.id).finish() 69 | } 70 | } 71 | 72 | impl Clone for Node 73 | where 74 | N: Clone, 75 | E: Clone, 76 | Ty: EdgeType, 77 | Ix: IndexType, 78 | D: DisplayNode, 79 | { 80 | fn clone(&self) -> Self { 81 | let idx = self.id().index(); 82 | Self { 83 | id: Some(NodeIndex::new(idx)), 84 | props: self.props.clone(), 85 | display: self.display.clone(), 86 | _marker: PhantomData, 87 | } 88 | } 89 | } 90 | 91 | impl Node 92 | where 93 | N: Clone, 94 | E: Clone, 95 | Ty: EdgeType, 96 | Ix: IndexType, 97 | D: DisplayNode, 98 | { 99 | /// Creates a new node with default properties 100 | pub fn new(payload: N) -> Self { 101 | let props = NodeProps { 102 | payload, 103 | location: Pos2::default(), 104 | color: Option::default(), 105 | label: String::default(), 106 | selected: bool::default(), 107 | dragged: bool::default(), 108 | }; 109 | 110 | Node::new_with_props(props) 111 | } 112 | 113 | /// Creates a new node with custom properties 114 | pub fn new_with_props(props: NodeProps) -> Self { 115 | let display = D::from(props.clone()); 116 | Self { 117 | props, 118 | display, 119 | 120 | id: Option::default(), 121 | _marker: PhantomData, 122 | } 123 | } 124 | 125 | pub fn props(&self) -> &NodeProps { 126 | &self.props 127 | } 128 | 129 | pub fn display(&self) -> &D { 130 | &self.display 131 | } 132 | 133 | pub fn display_mut(&mut self) -> &mut D { 134 | &mut self.display 135 | } 136 | 137 | #[allow(clippy::missing_panics_doc)] // TODO: Add panic message 138 | pub fn id(&self) -> NodeIndex { 139 | self.id.unwrap() 140 | } 141 | 142 | pub(crate) fn set_id(&mut self, id: NodeIndex) { 143 | self.id = Some(id); 144 | } 145 | 146 | pub fn payload(&self) -> &N { 147 | &self.props.payload 148 | } 149 | 150 | pub fn payload_mut(&mut self) -> &mut N { 151 | &mut self.props.payload 152 | } 153 | 154 | pub fn color(&self) -> Option { 155 | self.props.color() 156 | } 157 | 158 | pub fn set_color(&mut self, color: Color32) { 159 | self.props.color = Some(color); 160 | } 161 | 162 | pub fn location(&self) -> Pos2 { 163 | self.props.location() 164 | } 165 | 166 | pub fn set_location(&mut self, loc: Pos2) { 167 | self.props.location = loc; 168 | } 169 | 170 | pub fn selected(&self) -> bool { 171 | self.props.selected 172 | } 173 | 174 | pub fn set_selected(&mut self, selected: bool) { 175 | self.props.selected = selected; 176 | } 177 | 178 | pub fn dragged(&self) -> bool { 179 | self.props.dragged 180 | } 181 | 182 | pub fn set_dragged(&mut self, dragged: bool) { 183 | self.props.dragged = dragged; 184 | } 185 | 186 | pub fn label(&self) -> String { 187 | self.props.label.clone() 188 | } 189 | 190 | pub fn set_label(&mut self, label: String) { 191 | self.props.label = label; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/events/event.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 4 | pub struct PayloadPan { 5 | pub diff: [f32; 2], 6 | pub new_pan: [f32; 2], 7 | } 8 | 9 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 10 | pub struct PayloadZoom { 11 | pub diff: f32, 12 | pub new_zoom: f32, 13 | } 14 | 15 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 16 | pub struct PayloadNodeMove { 17 | pub id: usize, 18 | pub diff: [f32; 2], 19 | pub new_pos: [f32; 2], 20 | } 21 | 22 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 23 | pub struct PayloadNodeDragStart { 24 | pub id: usize, 25 | } 26 | 27 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 28 | pub struct PayloadNodeDragEnd { 29 | pub id: usize, 30 | } 31 | 32 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 33 | pub struct PayloadNodeSelect { 34 | pub id: usize, 35 | } 36 | 37 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 38 | pub struct PayloadNodeDeselect { 39 | pub id: usize, 40 | } 41 | 42 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 43 | pub struct PayloadNodeClick { 44 | pub id: usize, 45 | } 46 | 47 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 48 | pub struct PayloadNodeDoubleClick { 49 | pub id: usize, 50 | } 51 | 52 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 53 | pub struct PayloadEdgeClick { 54 | pub id: usize, 55 | } 56 | 57 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 58 | pub struct PayloadEdgeSelect { 59 | pub id: usize, 60 | } 61 | 62 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 63 | pub struct PayloadEdgeDeselect { 64 | pub id: usize, 65 | } 66 | 67 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 68 | pub enum Event { 69 | Pan(PayloadPan), 70 | Zoom(PayloadZoom), 71 | NodeMove(PayloadNodeMove), 72 | NodeDragStart(PayloadNodeDragStart), 73 | NodeDragEnd(PayloadNodeDragEnd), 74 | NodeSelect(PayloadNodeSelect), 75 | NodeDeselect(PayloadNodeDeselect), 76 | NodeClick(PayloadNodeClick), 77 | NodeDoubleClick(PayloadNodeDoubleClick), 78 | EdgeClick(PayloadEdgeClick), 79 | EdgeSelect(PayloadEdgeSelect), 80 | EdgeDeselect(PayloadEdgeDeselect), 81 | } 82 | -------------------------------------------------------------------------------- /src/events/mod.rs: -------------------------------------------------------------------------------- 1 | mod event; 2 | 3 | pub use event::{ 4 | Event, PayloadEdgeClick, PayloadEdgeDeselect, PayloadEdgeSelect, PayloadNodeClick, 5 | PayloadNodeDeselect, PayloadNodeDoubleClick, PayloadNodeDragEnd, PayloadNodeDragStart, 6 | PayloadNodeMove, PayloadNodeSelect, PayloadPan, PayloadZoom, 7 | }; 8 | -------------------------------------------------------------------------------- /src/graph.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use egui::Pos2; 4 | use petgraph::stable_graph::DefaultIx; 5 | use petgraph::Directed; 6 | 7 | use petgraph::graph::IndexType; 8 | use petgraph::{ 9 | stable_graph::{EdgeIndex, EdgeReference, NodeIndex, StableGraph}, 10 | visit::{EdgeRef, IntoEdgeReferences, IntoNodeReferences}, 11 | Direction, EdgeType, 12 | }; 13 | use serde::{Deserialize, Serialize}; 14 | 15 | use crate::draw::{DisplayEdge, DisplayNode}; 16 | use crate::{ 17 | default_edge_transform, default_node_transform, to_graph, DefaultEdgeShape, DefaultNodeShape, 18 | }; 19 | use crate::{metadata::Metadata, Edge, Node}; 20 | 21 | type StableGraphType = 22 | StableGraph, Edge, Ty, Ix>; 23 | 24 | /// Wrapper around [`petgraph::stable_graph::StableGraph`] compatible with [`super::GraphView`]. 25 | /// It is used to store graph data and provide access to it. 26 | #[derive(Debug, Clone, Serialize, Deserialize)] 27 | pub struct Graph< 28 | N = (), 29 | E = (), 30 | Ty = Directed, 31 | Ix = DefaultIx, 32 | Dn = DefaultNodeShape, 33 | De = DefaultEdgeShape, 34 | > where 35 | N: Clone, 36 | E: Clone, 37 | Ty: EdgeType, 38 | Ix: IndexType, 39 | Dn: DisplayNode, 40 | De: DisplayEdge, 41 | { 42 | pub g: StableGraphType, 43 | selected_nodes: Vec>, 44 | selected_edges: Vec>, 45 | dragged_node: Option>, 46 | } 47 | 48 | impl From<&StableGraph> for Graph 49 | where 50 | N: Clone, 51 | E: Clone, 52 | Ty: EdgeType, 53 | Ix: IndexType, 54 | Dn: DisplayNode, 55 | De: DisplayEdge, 56 | { 57 | fn from(g: &StableGraph) -> Self { 58 | to_graph(g) 59 | } 60 | } 61 | 62 | impl Graph 63 | where 64 | N: Clone, 65 | E: Clone, 66 | Ty: EdgeType, 67 | Ix: IndexType, 68 | Dn: DisplayNode, 69 | De: DisplayEdge, 70 | { 71 | pub fn new(g: StableGraphType) -> Self { 72 | Self { 73 | g, 74 | selected_nodes: Vec::default(), 75 | selected_edges: Vec::default(), 76 | dragged_node: Option::default(), 77 | } 78 | } 79 | 80 | /// Finds node by position. Can be optimized by using a spatial index like quad-tree if needed. 81 | pub fn node_by_screen_pos(&self, meta: &Metadata, screen_pos: Pos2) -> Option> { 82 | let pos_in_graph = meta.screen_to_canvas_pos(screen_pos); 83 | for (idx, node) in self.nodes_iter() { 84 | let display = node.display(); 85 | if display.is_inside(pos_in_graph) { 86 | return Some(idx); 87 | } 88 | } 89 | None 90 | } 91 | 92 | /// Finds edge by position. 93 | #[allow(clippy::missing_panics_doc)] // TODO: add panics doc 94 | pub fn edge_by_screen_pos(&self, meta: &Metadata, screen_pos: Pos2) -> Option> { 95 | let pos_in_graph = meta.screen_to_canvas_pos(screen_pos); 96 | for (idx, e) in self.edges_iter() { 97 | let Some((idx_start, idx_end)) = self.g.edge_endpoints(e.id()) else { 98 | continue; 99 | }; 100 | let start = self.g.node_weight(idx_start).unwrap(); 101 | let end = self.g.node_weight(idx_end).unwrap(); 102 | if e.display().is_inside(start, end, pos_in_graph) { 103 | return Some(idx); 104 | } 105 | } 106 | 107 | None 108 | } 109 | 110 | pub fn g(&mut self) -> &mut StableGraphType { 111 | &mut self.g 112 | } 113 | 114 | /// Adds node to graph setting default location and default label values 115 | #[allow(clippy::missing_panics_doc)] // TODO: add panics doc 116 | pub fn add_node(&mut self, payload: N) -> NodeIndex { 117 | self.add_node_custom(payload, default_node_transform) 118 | } 119 | 120 | #[allow(clippy::missing_panics_doc)] // TODO: add panics doc 121 | pub fn add_node_custom( 122 | &mut self, 123 | payload: N, 124 | node_transform: impl FnOnce(&mut Node), 125 | ) -> NodeIndex { 126 | let node = Node::new(payload); 127 | 128 | let idx = self.g.add_node(node); 129 | let graph_node = self.g.node_weight_mut(idx).unwrap(); 130 | 131 | graph_node.set_id(idx); 132 | 133 | node_transform(graph_node); 134 | 135 | idx 136 | } 137 | 138 | /// Adds node to graph setting custom location and default label value 139 | #[allow(clippy::missing_panics_doc)] // TODO: add panics doc 140 | pub fn add_node_with_location(&mut self, payload: N, location: Pos2) -> NodeIndex { 141 | self.add_node_custom(payload, |n: &mut Node| { 142 | n.set_location(location); 143 | }) 144 | } 145 | 146 | /// Adds node to graph setting default location and custom label value 147 | pub fn add_node_with_label(&mut self, payload: N, label: String) -> NodeIndex { 148 | self.add_node_custom(payload, |n: &mut Node| { 149 | n.set_label(label); 150 | }) 151 | } 152 | 153 | /// Adds node to graph setting custom location and custom label value 154 | #[allow(clippy::missing_panics_doc)] // TODO: add panics doc 155 | pub fn add_node_with_label_and_location( 156 | &mut self, 157 | payload: N, 158 | label: String, 159 | location: Pos2, 160 | ) -> NodeIndex { 161 | self.add_node_custom(payload, |n: &mut Node| { 162 | n.set_location(location); 163 | n.set_label(label); 164 | }) 165 | } 166 | 167 | /// Removes node by index. Returns removed node and None if it does not exist. 168 | pub fn remove_node(&mut self, idx: NodeIndex) -> Option> { 169 | // before removing nodes we need to remove all edges connected to it 170 | let neighbors = self.g.neighbors_undirected(idx).collect::>(); 171 | for n in &neighbors { 172 | self.remove_edges_between(idx, *n); 173 | self.remove_edges_between(*n, idx); 174 | } 175 | 176 | self.g.remove_node(idx) 177 | } 178 | 179 | /// Removes all edges between start and end node. Returns removed edges count. 180 | #[allow(clippy::missing_panics_doc)] // TODO: add panics doc 181 | pub fn remove_edges_between(&mut self, start: NodeIndex, end: NodeIndex) -> usize { 182 | let idxs = self 183 | .g 184 | .edges_connecting(start, end) 185 | .map(|e| e.id()) 186 | .collect::>(); 187 | if idxs.is_empty() { 188 | return 0; 189 | } 190 | 191 | let mut removed = 0; 192 | for e in &idxs { 193 | self.g.remove_edge(*e).unwrap(); 194 | removed += 1; 195 | } 196 | 197 | removed 198 | } 199 | 200 | /// Adds edge between start and end node with default label. 201 | #[allow(clippy::missing_panics_doc)] // TODO: add panics doc 202 | pub fn add_edge( 203 | &mut self, 204 | start: NodeIndex, 205 | end: NodeIndex, 206 | payload: E, 207 | ) -> EdgeIndex { 208 | self.add_edge_custom(start, end, payload, default_edge_transform) 209 | } 210 | 211 | /// Adds edge between start and end node with custom label setting correct order. 212 | #[allow(clippy::missing_panics_doc)] // TODO: add panics doc 213 | pub fn add_edge_with_label( 214 | &mut self, 215 | start: NodeIndex, 216 | end: NodeIndex, 217 | payload: E, 218 | label: String, 219 | ) -> EdgeIndex { 220 | self.add_edge_custom(start, end, payload, |e: &mut Edge| { 221 | e.set_label(label); 222 | }) 223 | } 224 | 225 | #[allow(clippy::missing_panics_doc)] // TODO: add panics doc 226 | pub fn add_edge_custom( 227 | &mut self, 228 | start: NodeIndex, 229 | end: NodeIndex, 230 | payload: E, 231 | edge_transform: impl FnOnce(&mut Edge), 232 | ) -> EdgeIndex { 233 | let order = self.g.edges_connecting(start, end).count(); 234 | 235 | let idx = self.g.add_edge(start, end, Edge::new(payload)); 236 | let e = self.g.edge_weight_mut(idx).unwrap(); 237 | 238 | e.set_id(idx); 239 | e.set_order(order); 240 | 241 | edge_transform(e); 242 | 243 | // check if we have 2 edges between start and end node with 0 order 244 | // in this case we need to set increase order by 1 for every edge 245 | 246 | let siblings_ids: Vec<_> = { 247 | let mut visited = HashSet::new(); 248 | self.g 249 | .edges_connecting(start, end) 250 | .chain(self.g.edges_connecting(end, start)) 251 | .filter(|e| visited.insert(e.id())) 252 | .map(|e| e.id()) 253 | .collect() 254 | }; 255 | 256 | let mut had_zero = false; 257 | let mut increase_order = false; 258 | for id in &siblings_ids { 259 | if let Some(edge) = self.g.edge_weight_mut(*id) { 260 | if edge.order() == 0 { 261 | if had_zero { 262 | increase_order = true; 263 | break; 264 | } 265 | 266 | had_zero = true; 267 | } 268 | } 269 | } 270 | 271 | if increase_order { 272 | for id in siblings_ids { 273 | if let Some(edge) = self.g.edge_weight_mut(id) { 274 | edge.set_order(edge.order() + 1); 275 | } 276 | } 277 | } 278 | 279 | idx 280 | } 281 | 282 | /// Removes edge by index and updates order of the siblings. 283 | /// Returns removed edge and None if it does not exist. 284 | pub fn remove_edge(&mut self, idx: EdgeIndex) -> Option> { 285 | let (start, end) = self.g.edge_endpoints(idx)?; 286 | let order = self.g.edge_weight(idx)?.order(); 287 | 288 | let payload = self.g.remove_edge(idx)?; 289 | 290 | let siblings = self 291 | .g 292 | .edges_connecting(start, end) 293 | .map(|edge_ref| edge_ref.id()) 294 | .collect::>(); 295 | 296 | // update order of siblings 297 | for s_idx in &siblings { 298 | let sibling_order = self.g.edge_weight(*s_idx)?.order(); 299 | if sibling_order < order { 300 | continue; 301 | } 302 | self.g.edge_weight_mut(*s_idx)?.set_order(sibling_order - 1); 303 | } 304 | 305 | Some(payload) 306 | } 307 | 308 | /// Returns iterator over all edges connecting start and end node. 309 | #[allow(clippy::type_complexity)] 310 | pub fn edges_connecting( 311 | &self, 312 | start: NodeIndex, 313 | end: NodeIndex, 314 | ) -> impl Iterator, &Edge)> { 315 | self.g 316 | .edges_connecting(start, end) 317 | .map(|e| (e.id(), e.weight())) 318 | } 319 | 320 | /// Provides iterator over all nodes and their indices. 321 | pub fn nodes_iter(&self) -> impl Iterator, &Node)> { 322 | self.g.node_references() 323 | } 324 | 325 | /// Provides iterator over all edges and their indices. 326 | #[allow(clippy::type_complexity)] 327 | pub fn edges_iter(&self) -> impl Iterator, &Edge)> { 328 | self.g.edge_references().map(|e| (e.id(), e.weight())) 329 | } 330 | 331 | pub fn node(&self, i: NodeIndex) -> Option<&Node> { 332 | self.g.node_weight(i) 333 | } 334 | 335 | pub fn edge(&self, i: EdgeIndex) -> Option<&Edge> { 336 | self.g.edge_weight(i) 337 | } 338 | 339 | pub fn edge_endpoints(&self, i: EdgeIndex) -> Option<(NodeIndex, NodeIndex)> { 340 | self.g.edge_endpoints(i) 341 | } 342 | 343 | pub fn node_mut(&mut self, i: NodeIndex) -> Option<&mut Node> { 344 | self.g.node_weight_mut(i) 345 | } 346 | 347 | pub fn edge_mut(&mut self, i: EdgeIndex) -> Option<&mut Edge> { 348 | self.g.edge_weight_mut(i) 349 | } 350 | 351 | pub fn is_directed(&self) -> bool { 352 | self.g.is_directed() 353 | } 354 | 355 | pub fn edges_num(&self, idx: NodeIndex) -> usize { 356 | self.g.edges(idx).count() 357 | } 358 | 359 | pub fn edges_directed( 360 | &self, 361 | idx: NodeIndex, 362 | dir: Direction, 363 | ) -> impl Iterator, Ix>> { 364 | self.g.edges_directed(idx, dir) 365 | } 366 | 367 | pub fn selected_nodes(&self) -> &[NodeIndex] { 368 | &self.selected_nodes 369 | } 370 | 371 | pub fn set_selected_nodes(&mut self, nodes: Vec>) { 372 | self.selected_nodes = nodes; 373 | } 374 | 375 | pub fn selected_edges(&self) -> &[EdgeIndex] { 376 | &self.selected_edges 377 | } 378 | 379 | pub fn set_selected_edges(&mut self, edges: Vec>) { 380 | self.selected_edges = edges; 381 | } 382 | 383 | pub fn dragged_node(&self) -> Option> { 384 | self.dragged_node 385 | } 386 | 387 | pub fn set_dragged_node(&mut self, node: Option>) { 388 | self.dragged_node = node; 389 | } 390 | 391 | pub fn edge_count(&self) -> usize { 392 | self.g.edge_count() 393 | } 394 | 395 | pub fn node_count(&self) -> usize { 396 | self.g.node_count() 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /src/helpers.rs: -------------------------------------------------------------------------------- 1 | use crate::{DisplayEdge, DisplayNode, Edge, Graph, Node}; 2 | use egui::Vec2; 3 | use petgraph::{ 4 | graph::IndexType, 5 | stable_graph::{EdgeIndex, NodeIndex, StableGraph}, 6 | visit::IntoNodeReferences, 7 | EdgeType, 8 | }; 9 | use rand::Rng; 10 | use std::collections::HashMap; 11 | 12 | /// Helper function which adds user's node to the [`super::Graph`] instance. 13 | /// 14 | /// If graph is not empty it picks any node position and adds new node in the vicinity of it. 15 | #[deprecated(since = "0.25.0", note = "please use `super::Graph::add_node` instead")] 16 | pub fn add_node(g: &mut Graph, n: &N) -> NodeIndex 17 | where 18 | N: Clone, 19 | E: Clone, 20 | Ty: EdgeType, 21 | Ix: IndexType, 22 | Dn: DisplayNode, 23 | De: DisplayEdge, 24 | { 25 | #[allow(deprecated)] 26 | add_node_custom(g, n, default_node_transform) 27 | } 28 | 29 | /// Helper function which adds user's node to the [`super::Graph`] instance with custom node transform function. 30 | /// 31 | /// If graph is not empty it picks any node position and adds new node in the vicinity of it. 32 | #[deprecated( 33 | since = "0.25.0", 34 | note = "please use `super::Graph::add_node_custom` instead" 35 | )] 36 | pub fn add_node_custom( 37 | g: &mut Graph, 38 | n: &N, 39 | node_transform: impl FnOnce(&mut Node), 40 | ) -> NodeIndex 41 | where 42 | N: Clone, 43 | E: Clone, 44 | Ty: EdgeType, 45 | Ix: IndexType, 46 | Dn: DisplayNode, 47 | De: DisplayEdge, 48 | { 49 | g.add_node_custom(n.clone(), node_transform) 50 | } 51 | 52 | /// Helper function which adds user's edge to the [`super::Graph`] instance. 53 | #[deprecated(since = "0.25.0", note = "please use `super::Graph::add_edge` instead")] 54 | pub fn add_edge( 55 | g: &mut Graph, 56 | start: NodeIndex, 57 | end: NodeIndex, 58 | e: &E, 59 | ) -> EdgeIndex 60 | where 61 | N: Clone, 62 | E: Clone, 63 | Ty: EdgeType, 64 | Ix: IndexType, 65 | Dn: DisplayNode, 66 | De: DisplayEdge, 67 | { 68 | #[allow(deprecated)] 69 | add_edge_custom( 70 | g, 71 | start, 72 | end, 73 | e, 74 | default_edge_transform::, 75 | ) 76 | } 77 | 78 | /// Helper function which adds user's edge to the [`super::Graph`] instance with custom edge transform function. 79 | #[deprecated( 80 | since = "0.25.0", 81 | note = "please use `super::Graph::add_edge_custom` instead" 82 | )] 83 | pub fn add_edge_custom( 84 | g: &mut Graph, 85 | start: NodeIndex, 86 | end: NodeIndex, 87 | e: &E, 88 | edge_transform: impl FnOnce(&mut Edge), 89 | ) -> EdgeIndex 90 | where 91 | N: Clone, 92 | E: Clone, 93 | Ty: EdgeType, 94 | Ix: IndexType, 95 | Dn: DisplayNode, 96 | De: DisplayEdge, 97 | { 98 | g.add_edge_custom(start, end, e.clone(), edge_transform) 99 | } 100 | 101 | /// Helper function which transforms [`petgraph::stable_graph::StableGraph`] into the [`super::Graph`] required by the [`super::GraphView`] widget. 102 | /// 103 | /// The function creates a new `StableGraph` where nodes and edges are represented by [`super::Node`] and [`super::Edge`] respectively. 104 | /// New nodes and edges are created with [`default_node_transform`] and [`default_edge_transform`] functions. 105 | /// If you want to define custom transformation procedures (e.g. to use custom label for nodes), use [`to_graph_custom`] instead. 106 | /// 107 | /// # Example 108 | /// ``` 109 | /// use petgraph::stable_graph::StableGraph; 110 | /// use egui_graphs::{to_graph, DefaultNodeShape, DefaultEdgeShape, Graph}; 111 | /// use egui::Pos2; 112 | /// 113 | /// let mut g: StableGraph<&str, &str> = StableGraph::new(); 114 | /// let node1 = g.add_node("A"); 115 | /// let node2 = g.add_node("B"); 116 | /// g.add_edge(node1, node2, "edge1"); 117 | /// 118 | /// let result: Graph<_, _, _, _, DefaultNodeShape, DefaultEdgeShape> = to_graph(&g); 119 | /// 120 | /// assert_eq!(result.g.node_count(), 2); 121 | /// assert_eq!(result.g.edge_count(), 1); 122 | /// 123 | /// let mut indxs = result.g.node_indices(); 124 | /// let result_node1 = indxs.next().unwrap(); 125 | /// let result_node2 = indxs.next().unwrap(); 126 | /// assert_eq!(*result.g.node_weight(result_node1).unwrap().payload(), "A"); 127 | /// assert_eq!(*result.g.node_weight(result_node2).unwrap().payload(), "B"); 128 | /// 129 | /// assert_eq!(*result.g.edge_weight(result.g.edge_indices().next().unwrap()).unwrap().payload(), "edge1"); 130 | /// 131 | /// assert_eq!(*result.g.node_weight(result_node1).unwrap().label().clone(), format!("node {}", result_node1.index())); 132 | /// assert_eq!(*result.g.node_weight(result_node2).unwrap().label().clone(), format!("node {}", result_node2.index())); 133 | /// ``` 134 | pub fn to_graph(g: &StableGraph) -> Graph 135 | where 136 | N: Clone, 137 | E: Clone, 138 | Ty: EdgeType, 139 | Ix: IndexType, 140 | Dn: DisplayNode, 141 | De: DisplayEdge, 142 | { 143 | transform(g, &mut default_node_transform, &mut default_edge_transform) 144 | } 145 | 146 | /// The same as [`to_graph`], but allows to define custom transformation procedures for nodes and edges. 147 | pub fn to_graph_custom( 148 | g: &StableGraph, 149 | mut node_transform: impl FnMut(&mut Node), 150 | mut edge_transform: impl FnMut(&mut Edge), 151 | ) -> Graph 152 | where 153 | N: Clone, 154 | E: Clone, 155 | Ty: EdgeType, 156 | Ix: IndexType, 157 | Dn: DisplayNode, 158 | De: DisplayEdge, 159 | { 160 | transform(g, &mut node_transform, &mut edge_transform) 161 | } 162 | 163 | fn transform( 164 | input: &StableGraph, 165 | node_transform: &mut impl FnMut(&mut Node), 166 | edge_transform: &mut impl FnMut(&mut Edge), 167 | ) -> Graph 168 | where 169 | N: Clone, 170 | E: Clone, 171 | Ty: EdgeType, 172 | Ix: IndexType, 173 | Dn: DisplayNode, 174 | De: DisplayEdge, 175 | { 176 | let g_stable = 177 | StableGraph::, Edge, Ty, Ix>::default(); 178 | 179 | let mut g = Graph::new(g_stable); 180 | 181 | let nidx_by_input_nidx = input 182 | .node_references() 183 | .map(|(input_n_idx, input_n)| { 184 | ( 185 | input_n_idx, 186 | g.add_node_custom(input_n.clone(), &mut *node_transform), 187 | ) 188 | }) 189 | .collect::, NodeIndex>>(); 190 | 191 | input.edge_indices().for_each(|input_e_idx| { 192 | let (input_source_n_idx, input_target_n_idx) = input.edge_endpoints(input_e_idx).unwrap(); 193 | let input_e = input.edge_weight(input_e_idx).unwrap(); 194 | 195 | let input_source_n = *nidx_by_input_nidx.get(&input_source_n_idx).unwrap(); 196 | let input_target_n = *nidx_by_input_nidx.get(&input_target_n_idx).unwrap(); 197 | 198 | g.add_edge_custom( 199 | input_source_n, 200 | input_target_n, 201 | input_e.clone(), 202 | &mut *edge_transform, 203 | ); 204 | }); 205 | 206 | g 207 | } 208 | 209 | pub fn node_size>( 210 | node: &Node, 211 | dir: Vec2, 212 | ) -> f32 { 213 | let connector_left = node.display().closest_boundary_point(dir); 214 | let connector_right = node.display().closest_boundary_point(-dir); 215 | 216 | ((connector_right.to_vec2() - connector_left.to_vec2()) / 2.).length() 217 | } 218 | 219 | pub fn random_graph(num_nodes: usize, num_edges: usize) -> Graph { 220 | let mut rng = rand::rng(); 221 | let mut graph = StableGraph::new(); 222 | 223 | for _ in 0..num_nodes { 224 | graph.add_node(()); 225 | } 226 | 227 | for _ in 0..num_edges { 228 | let source = rng.random_range(0..num_nodes); 229 | let target = rng.random_range(0..num_nodes); 230 | 231 | graph.add_edge(NodeIndex::new(source), NodeIndex::new(target), ()); 232 | } 233 | 234 | to_graph(&graph) 235 | } 236 | 237 | /// Default edge transform function. Keeps original data and creates a new edge. 238 | pub fn default_edge_transform< 239 | N: Clone, 240 | E: Clone, 241 | Ty: EdgeType, 242 | Ix: IndexType, 243 | Dn: DisplayNode, 244 | D: DisplayEdge, 245 | >( 246 | edge: &mut Edge, 247 | ) { 248 | edge.set_label(format!("edge {}", edge.id().index())); 249 | } 250 | 251 | /// Default node transform function. Keeps original data and creates a new node with a random location and 252 | /// label equal to the index of the node in the graph. 253 | pub fn default_node_transform< 254 | N: Clone, 255 | E: Clone, 256 | Ty: EdgeType, 257 | Ix: IndexType, 258 | D: DisplayNode, 259 | >( 260 | node: &mut Node, 261 | ) { 262 | node.set_label(format!("node {}", node.id().index())); 263 | } 264 | 265 | #[cfg(test)] 266 | mod tests { 267 | use crate::DefaultEdgeShape; 268 | use crate::DefaultNodeShape; 269 | 270 | use super::*; 271 | use petgraph::Directed; 272 | use petgraph::Undirected; 273 | 274 | #[test] 275 | fn test_to_graph_directed() { 276 | let mut user_g: StableGraph<_, _, Directed> = StableGraph::new(); 277 | let n1 = user_g.add_node("Node1"); 278 | let n2 = user_g.add_node("Node2"); 279 | user_g.add_edge(n1, n2, "Edge1"); 280 | 281 | let input_g = to_graph::<_, _, _, _, DefaultNodeShape, DefaultEdgeShape>(&user_g); 282 | 283 | assert_eq!(user_g.node_count(), input_g.g.node_count()); 284 | assert_eq!(user_g.edge_count(), input_g.g.edge_count()); 285 | assert_eq!(user_g.is_directed(), input_g.is_directed()); 286 | 287 | for (user_idx, input_idx) in input_g.g.node_indices().zip(user_g.node_indices()) { 288 | let user_n = user_g.node_weight(user_idx).unwrap(); 289 | let input_n = input_g.g.node_weight(input_idx).unwrap(); 290 | 291 | assert_eq!(*input_n.payload(), *user_n); 292 | assert_eq!(*input_n.label(), format!("node {}", user_idx.index())); 293 | 294 | assert!(!input_n.selected()); 295 | assert!(!input_n.dragged()); 296 | } 297 | } 298 | 299 | #[test] 300 | fn test_to_graph_undirected() { 301 | let mut user_g: StableGraph<_, _, Undirected> = StableGraph::default(); 302 | let n1 = user_g.add_node("Node1"); 303 | let n2 = user_g.add_node("Node2"); 304 | user_g.add_edge(n1, n2, "Edge1"); 305 | 306 | let input_g = to_graph::<_, _, _, _, DefaultNodeShape, DefaultEdgeShape>(&user_g); 307 | 308 | assert_eq!(user_g.node_count(), input_g.g.node_count()); 309 | assert_eq!(user_g.edge_count(), input_g.g.edge_count()); 310 | assert_eq!(user_g.is_directed(), input_g.is_directed()); 311 | 312 | for (user_idx, input_idx) in input_g.g.node_indices().zip(user_g.node_indices()) { 313 | let user_n = user_g.node_weight(user_idx).unwrap(); 314 | let input_n = input_g.g.node_weight(input_idx).unwrap(); 315 | 316 | assert_eq!(*input_n.payload(), *user_n); 317 | assert_eq!(*input_n.label(), format!("node {}", user_idx.index())); 318 | 319 | assert!(!input_n.selected()); 320 | assert!(!input_n.dragged()); 321 | } 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /src/layouts/hierarchical/layout.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use egui::Pos2; 4 | use petgraph::{ 5 | csr::IndexType, 6 | stable_graph::NodeIndex, 7 | Direction::{Incoming, Outgoing}, 8 | EdgeType, 9 | }; 10 | use serde::{Deserialize, Serialize}; 11 | 12 | use crate::{ 13 | layouts::{Layout, LayoutState}, 14 | DisplayEdge, DisplayNode, Graph, 15 | }; 16 | 17 | const ROW_DIST: usize = 50; 18 | const NODE_DIST: usize = 50; 19 | 20 | #[derive(Debug, Default, Clone, Serialize, Deserialize)] 21 | pub struct State { 22 | triggered: bool, 23 | } 24 | 25 | impl LayoutState for State {} 26 | 27 | #[derive(Debug, Default)] 28 | pub struct Hierarchical { 29 | state: State, 30 | } 31 | 32 | impl Layout for Hierarchical { 33 | fn next(&mut self, g: &mut Graph) 34 | where 35 | N: Clone, 36 | E: Clone, 37 | Ty: EdgeType, 38 | Ix: IndexType, 39 | Dn: DisplayNode, 40 | De: DisplayEdge, 41 | { 42 | if self.state.triggered { 43 | return; 44 | } 45 | 46 | let mut visited = HashSet::new(); 47 | let mut max_col = 0; 48 | g.g.externals(Incoming) 49 | .collect::>>() 50 | .iter() 51 | .enumerate() 52 | .for_each(|(i, root_idx)| { 53 | visited.insert(*root_idx); 54 | 55 | let curr_max_col = build_tree(g, &mut visited, root_idx, 0, i); 56 | if curr_max_col > max_col { 57 | max_col = curr_max_col; 58 | } 59 | }); 60 | 61 | self.state.triggered = true; 62 | } 63 | 64 | fn state(&self) -> State { 65 | self.state.clone() 66 | } 67 | 68 | fn from_state(state: State) -> impl Layout { 69 | Hierarchical { state } 70 | } 71 | } 72 | 73 | fn build_tree( 74 | g: &mut Graph, 75 | visited: &mut HashSet>, 76 | root_idx: &NodeIndex, 77 | start_row: usize, 78 | start_col: usize, 79 | ) -> usize 80 | where 81 | N: Clone, 82 | E: Clone, 83 | Ty: EdgeType, 84 | Ix: IndexType, 85 | Dn: DisplayNode, 86 | De: DisplayEdge, 87 | { 88 | let y = start_row * ROW_DIST; 89 | let x = start_col * NODE_DIST; 90 | 91 | let node = &mut g.g[*root_idx]; 92 | node.set_location(Pos2::new(x as f32, y as f32)); 93 | 94 | let mut max_col = start_col; 95 | g.g.neighbors_directed(*root_idx, Outgoing) 96 | .collect::>>() 97 | .iter() 98 | .enumerate() 99 | .for_each(|(i, neighbour_idx)| { 100 | if visited.contains(neighbour_idx) { 101 | return; 102 | } 103 | 104 | visited.insert(*neighbour_idx); 105 | 106 | let curr_max_col = build_tree(g, visited, neighbour_idx, start_row + 1, start_col + i); 107 | if curr_max_col > max_col { 108 | max_col = curr_max_col; 109 | } 110 | }); 111 | 112 | max_col 113 | } 114 | -------------------------------------------------------------------------------- /src/layouts/hierarchical/mod.rs: -------------------------------------------------------------------------------- 1 | mod layout; 2 | 3 | pub use layout::{Hierarchical, State}; 4 | -------------------------------------------------------------------------------- /src/layouts/layout.rs: -------------------------------------------------------------------------------- 1 | use egui::util::id_type_map::SerializableAny; 2 | use petgraph::{stable_graph::IndexType, EdgeType}; 3 | 4 | use crate::{DisplayEdge, DisplayNode, Graph}; 5 | 6 | pub trait LayoutState: SerializableAny + Default {} 7 | 8 | pub trait Layout: Default 9 | where 10 | S: LayoutState, 11 | { 12 | /// Creates a new layout from the given state. State is loaded and saved on every frame. 13 | fn from_state(state: S) -> impl Layout; 14 | 15 | /// Called on every frame. It should update the graph layout aka nodes locations. 16 | fn next(&mut self, g: &mut Graph) 17 | where 18 | N: Clone, 19 | E: Clone, 20 | Ty: EdgeType, 21 | Ix: IndexType, 22 | Dn: DisplayNode, 23 | De: DisplayEdge; 24 | 25 | /// Returns the current state of the layout. 26 | fn state(&self) -> S; 27 | } 28 | -------------------------------------------------------------------------------- /src/layouts/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod hierarchical; 2 | pub mod random; 3 | 4 | mod layout; 5 | pub use layout::{Layout, LayoutState}; 6 | -------------------------------------------------------------------------------- /src/layouts/random/layout.rs: -------------------------------------------------------------------------------- 1 | use egui::Pos2; 2 | use petgraph::stable_graph::IndexType; 3 | use rand::Rng; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::{ 7 | layouts::{Layout, LayoutState}, 8 | Graph, 9 | }; 10 | const SPAWN_SIZE: f32 = 250.; 11 | 12 | #[derive(Debug, Default, Clone, Serialize, Deserialize)] 13 | pub struct State { 14 | triggered: bool, 15 | } 16 | 17 | impl LayoutState for State {} 18 | 19 | /// Randomly places nodes on the canvas. Does not override existing locations. Applies once. 20 | #[derive(Debug, Default)] 21 | pub struct Random { 22 | state: State, 23 | } 24 | 25 | impl Layout for Random { 26 | fn next(&mut self, g: &mut Graph) 27 | where 28 | N: Clone, 29 | E: Clone, 30 | Ty: petgraph::EdgeType, 31 | Ix: IndexType, 32 | Dn: crate::DisplayNode, 33 | De: crate::DisplayEdge, 34 | { 35 | if self.state.triggered { 36 | return; 37 | } 38 | 39 | let mut rng = rand::rng(); 40 | for node in g.g.node_weights_mut() { 41 | node.set_location(Pos2::new( 42 | rng.random_range(0. ..SPAWN_SIZE), 43 | rng.random_range(0. ..SPAWN_SIZE), 44 | )); 45 | } 46 | 47 | self.state.triggered = true; 48 | } 49 | 50 | fn state(&self) -> State { 51 | self.state.clone() 52 | } 53 | 54 | fn from_state(state: State) -> impl Layout { 55 | Self { state } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/layouts/random/mod.rs: -------------------------------------------------------------------------------- 1 | mod layout; 2 | 3 | pub use layout::{Random, State}; 4 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod draw; 2 | mod elements; 3 | mod graph; 4 | mod graph_view; 5 | mod helpers; 6 | mod layouts; 7 | mod metadata; 8 | mod settings; 9 | 10 | pub use draw::{DefaultEdgeShape, DefaultNodeShape, DisplayEdge, DisplayNode, DrawContext}; 11 | pub use elements::{Edge, EdgeProps, Node, NodeProps}; 12 | pub use graph::Graph; 13 | pub use graph_view::{DefaultGraphView, GraphView}; 14 | #[allow(deprecated)] 15 | pub use helpers::{ 16 | add_edge, add_edge_custom, add_node, add_node_custom, default_edge_transform, 17 | default_node_transform, node_size, random_graph, to_graph, to_graph_custom, 18 | }; 19 | pub use layouts::hierarchical::{ 20 | Hierarchical as LayoutHierarchical, State as LayoutStateHierarchical, 21 | }; 22 | pub use layouts::random::{Random as LayoutRandom, State as LayoutStateRandom}; 23 | pub use metadata::Metadata; 24 | pub use settings::{SettingsInteraction, SettingsNavigation, SettingsStyle}; 25 | 26 | #[cfg(feature = "events")] 27 | pub mod events; 28 | -------------------------------------------------------------------------------- /src/metadata.rs: -------------------------------------------------------------------------------- 1 | use egui::{Id, Pos2, Rect, Vec2}; 2 | use petgraph::{stable_graph::IndexType, EdgeType}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::{node_size, DisplayNode, Node}; 6 | 7 | const KEY: &str = "egui_graphs_metadata"; 8 | 9 | #[derive(Clone, Debug, Serialize, Deserialize)] 10 | struct Bounds { 11 | min: Vec2, 12 | max: Vec2, 13 | } 14 | 15 | impl Default for Bounds { 16 | fn default() -> Self { 17 | Self { 18 | min: Vec2::new(f32::MAX, f32::MAX), 19 | max: Vec2::new(f32::MIN, f32::MIN), 20 | } 21 | } 22 | } 23 | 24 | impl Bounds { 25 | pub fn compute_next< 26 | N: Clone, 27 | E: Clone, 28 | Ty: EdgeType, 29 | Ix: IndexType, 30 | D: DisplayNode, 31 | >( 32 | &mut self, 33 | n: &Node, 34 | ) { 35 | let size = node_size(n, Vec2::new(0., 1.)); 36 | let loc = n.location(); 37 | if loc.x + size < self.min.x { 38 | self.min.x = loc.x + size; 39 | } 40 | if loc.x + size > self.max.x { 41 | self.max.x = loc.x + size; 42 | } 43 | if loc.y - size < self.min.y { 44 | self.min.y = loc.y - size; 45 | } 46 | if loc.y + size > self.max.y { 47 | self.max.y = loc.y + size; 48 | } 49 | } 50 | } 51 | 52 | #[derive(Clone, Debug, Serialize, Deserialize)] 53 | pub struct Metadata { 54 | /// Whether the frame is the first one 55 | pub first_frame: bool, 56 | /// Current zoom factor 57 | pub zoom: f32, 58 | /// Current pan offset 59 | pub pan: Vec2, 60 | /// Top left position of widget 61 | pub top_left: Pos2, 62 | 63 | /// State of bounds iteration 64 | bounds: Bounds, 65 | } 66 | 67 | impl Default for Metadata { 68 | fn default() -> Self { 69 | Self { 70 | first_frame: true, 71 | zoom: 1., 72 | pan: Vec2::default(), 73 | top_left: Pos2::default(), 74 | bounds: Bounds::default(), 75 | } 76 | } 77 | } 78 | 79 | impl Metadata { 80 | pub fn load(ui: &egui::Ui) -> Self { 81 | ui.data_mut(|data| { 82 | data.get_persisted::(Id::new(KEY)) 83 | .unwrap_or_default() 84 | }) 85 | } 86 | 87 | pub fn save(self, ui: &mut egui::Ui) { 88 | ui.data_mut(|data| { 89 | data.insert_persisted(Id::new(KEY), self); 90 | }); 91 | } 92 | 93 | pub fn canvas_to_screen_pos(&self, pos: Pos2) -> Pos2 { 94 | (pos.to_vec2() * self.zoom + self.pan).to_pos2() 95 | } 96 | 97 | pub fn canvas_to_screen_size(&self, size: f32) -> f32 { 98 | size * self.zoom 99 | } 100 | 101 | pub fn screen_to_canvas_pos(&self, pos: Pos2) -> Pos2 { 102 | ((pos.to_vec2() - self.pan) / self.zoom).to_pos2() 103 | } 104 | 105 | pub fn comp_iter_bounds< 106 | N: Clone, 107 | E: Clone, 108 | Ty: EdgeType, 109 | Ix: IndexType, 110 | D: DisplayNode, 111 | >( 112 | &mut self, 113 | n: &Node, 114 | ) { 115 | self.bounds.compute_next(n); 116 | } 117 | 118 | /// Returns bounding rect of the graph. 119 | pub fn graph_bounds(&self) -> Rect { 120 | Rect::from_min_max(self.bounds.min.to_pos2(), self.bounds.max.to_pos2()) 121 | } 122 | 123 | /// Resets the bounds iterator. 124 | pub fn reset_bounds(&mut self) { 125 | self.bounds = Bounds::default(); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/settings.rs: -------------------------------------------------------------------------------- 1 | /// Represents graph interaction settings. 2 | #[derive(Debug, Clone, Default)] 3 | pub struct SettingsInteraction { 4 | pub(crate) dragging_enabled: bool, 5 | pub(crate) node_clicking_enabled: bool, 6 | pub(crate) node_selection_enabled: bool, 7 | pub(crate) node_selection_multi_enabled: bool, 8 | pub(crate) edge_clicking_enabled: bool, 9 | pub(crate) edge_selection_enabled: bool, 10 | pub(crate) edge_selection_multi_enabled: bool, 11 | } 12 | 13 | impl SettingsInteraction { 14 | /// Creates new [`SettingsInteraction`] with default values. 15 | pub fn new() -> Self { 16 | Self::default() 17 | } 18 | 19 | /// Node dragging. To drag a node with your mouse or finger. 20 | /// 21 | /// Default: `false` 22 | pub fn with_dragging_enabled(mut self, enabled: bool) -> Self { 23 | self.dragging_enabled = enabled; 24 | self 25 | } 26 | 27 | /// Allows clicking on nodes. 28 | /// 29 | /// Default: `false` 30 | pub fn with_node_clicking_enabled(mut self, enabled: bool) -> Self { 31 | self.node_clicking_enabled = enabled; 32 | self 33 | } 34 | 35 | /// Selects clicked node, enables clicks. 36 | /// 37 | /// Select by clicking on node, deselect by clicking again. 38 | /// 39 | /// Clicking on empty space deselects all nodes. 40 | /// 41 | /// Default: `false` 42 | pub fn with_node_selection_enabled(mut self, enabled: bool) -> Self { 43 | self.node_selection_enabled = enabled; 44 | self 45 | } 46 | 47 | /// Multiselection for nodes, enables click and select. 48 | /// 49 | /// Default: `false` 50 | pub fn with_node_selection_multi_enabled(mut self, enabled: bool) -> Self { 51 | self.node_selection_multi_enabled = enabled; 52 | self 53 | } 54 | 55 | /// Allows clicking on edges. 56 | /// 57 | /// Default: `false` 58 | pub fn with_edge_clicking_enabled(mut self, enabled: bool) -> Self { 59 | self.edge_clicking_enabled = enabled; 60 | self 61 | } 62 | 63 | /// Selects clicked edge, enables clicks. 64 | /// 65 | /// Select by clicking on a edge, deselect by clicking again. 66 | /// 67 | /// Clicking on empty space deselects all edges. 68 | /// 69 | /// Default: `false` 70 | pub fn with_edge_selection_enabled(mut self, enabled: bool) -> Self { 71 | self.edge_selection_enabled = enabled; 72 | self 73 | } 74 | 75 | /// Multiselection for edges, enables click and select. 76 | /// 77 | /// Default: `false` 78 | pub fn with_edge_selection_multi_enabled(mut self, enabled: bool) -> Self { 79 | self.edge_selection_multi_enabled = enabled; 80 | self 81 | } 82 | } 83 | 84 | /// Represents graph navigation settings. 85 | #[derive(Debug, Clone)] 86 | pub struct SettingsNavigation { 87 | pub(crate) fit_to_screen_enabled: bool, 88 | pub(crate) zoom_and_pan_enabled: bool, 89 | pub(crate) screen_padding: f32, 90 | pub(crate) zoom_speed: f32, 91 | } 92 | 93 | impl Default for SettingsNavigation { 94 | fn default() -> Self { 95 | Self { 96 | screen_padding: 0.3, 97 | zoom_speed: 0.1, 98 | fit_to_screen_enabled: true, 99 | zoom_and_pan_enabled: false, 100 | } 101 | } 102 | } 103 | 104 | impl SettingsNavigation { 105 | /// Creates new [`SettingsNavigation`] with default values. 106 | pub fn new() -> Self { 107 | Self::default() 108 | } 109 | 110 | /// Fits the graph to the screen. 111 | /// 112 | /// With this enabled, the graph will be scaled and panned to fit the screen on every frame. 113 | /// 114 | /// You can configure the padding around the graph with `screen_padding` setting. 115 | /// 116 | /// Default: `true` 117 | pub fn with_fit_to_screen_enabled(mut self, enabled: bool) -> Self { 118 | self.fit_to_screen_enabled = enabled; 119 | self 120 | } 121 | 122 | /// Zoom with ctrl + mouse wheel, pan with mouse drag. 123 | /// 124 | /// Default: `false` 125 | pub fn with_zoom_and_pan_enabled(mut self, enabled: bool) -> Self { 126 | self.zoom_and_pan_enabled = enabled; 127 | self 128 | } 129 | 130 | /// Padding around the graph when fitting to the screen. 131 | pub fn with_screen_padding(mut self, padding: f32) -> Self { 132 | self.screen_padding = padding; 133 | self 134 | } 135 | 136 | /// Controls the speed of the zoom. 137 | pub fn with_zoom_speed(mut self, speed: f32) -> Self { 138 | self.zoom_speed = speed; 139 | self 140 | } 141 | } 142 | 143 | /// `SettingsStyle` stores settings for the style of the graph. 144 | #[derive(Debug, Clone, Default)] 145 | pub struct SettingsStyle { 146 | pub(crate) labels_always: bool, 147 | } 148 | 149 | impl SettingsStyle { 150 | /// Creates new [`SettingsStyle`] with default values. 151 | /// ``` 152 | /// use egui_graphs::SettingsStyle; 153 | /// let settings = SettingsStyle::new(); 154 | /// ``` 155 | pub fn new() -> Self { 156 | Self::default() 157 | } 158 | 159 | /// Whether to show labels always or when interacted. 160 | /// 161 | /// Default is false. 162 | pub fn with_labels_always(mut self, always: bool) -> Self { 163 | self.labels_always = always; 164 | self 165 | } 166 | } 167 | --------------------------------------------------------------------------------