Skip to content

Commit

Permalink
Drawing refactor (#108)
Browse files Browse the repository at this point in the history
Refactored drawing and custom drawing approach.
The aim is to make drawing of custom shapes more transparent and solve
interaction and visualization of custom drawn shapes in graph context
described here #99.

Introduction of `NodeDisplay` trait which allows to simply switch draw
implementation for any element node or edge.

Breaking:
* GraphView generic structure
  * New generics for custom drawing functions
  * Explicit
* WidgetState -> DrawContext
* `Node` and `Edge` api changes
  * Data -> Payload
* Removed radius from node and style information from edge as it is the
scope of `NodeDisplay` or `EdgeDisplay` implementations
* Transform api
* Custom draw api
  • Loading branch information
blitzarx1 authored Nov 17, 2023
1 parent 7c4a15c commit ddd68cc
Show file tree
Hide file tree
Showing 29 changed files with 1,226 additions and 1,012 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "egui_graphs"
version = "0.15.0"
version = "0.16.0-beta.0"
authors = ["Dmitrii Samsonov <[email protected]>"]
license = "MIT"
homepage = "https://github.com/blitzarx1/egui_graphs"
Expand All @@ -11,7 +11,10 @@ edition = "2021"
[dependencies]
egui = { version = "0.23", default-features = false }
rand = "0.8"
petgraph = { version = "0.6", default-features = false, features = ["stable_graph", "matrix_graph"] }
petgraph = { version = "0.6", default-features = false, features = [
"stable_graph",
"matrix_graph",
] }
crossbeam = { version = "0.8", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
serde_json = { version = "1.0", optional = true }
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The project implements a Widget for the egui framework, enabling easy visualizat
- [x] Style configuration via egui context styles;
- [x] Dark/Light theme support via egui context styles;
- [x] Events reporting to extend the graph functionality by the user handling them;
- [ ] Edge labels (for the moment there is a `custom_draw` example which demonstrates labels drawing for edges);
- [ ] Edge labels;

## Status
The project is on track for a stable release v1.0.0. For the moment, breaking releases are still possible.
Expand Down Expand Up @@ -80,7 +80,14 @@ Now, lets implement the `update()` function for the `BasicApp`. This function cr
impl App for BasicApp {
fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.add(&mut GraphView::new(&mut self.g));
ui.add(&mut GraphView::<
_,
_,
_,
_,
DefaultNodeShape,
DefaultEdgeShape<_>,
>new(&mut self.g));
});
}
}
Expand Down
18 changes: 11 additions & 7 deletions examples/basic/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
use eframe::{run_native, App, CreationContext};
use egui::Context;
use egui_graphs::{Graph, GraphView};
use petgraph::{
stable_graph::{DefaultIx, StableGraph},
Directed,
};
use egui_graphs::{DefaultEdgeShape, DefaultNodeShape, Graph, GraphView};
use petgraph::stable_graph::StableGraph;

pub struct BasicApp {
g: Graph<(), (), Directed, DefaultIx>,
g: Graph<(), ()>,
}

impl BasicApp {
Expand All @@ -20,7 +17,14 @@ impl BasicApp {
impl App for BasicApp {
fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.add(&mut GraphView::new(&mut self.g));
ui.add(&mut GraphView::<
_,
_,
_,
_,
DefaultNodeShape,
DefaultEdgeShape<_>,
>::new(&mut self.g));
});
}
}
Expand Down
71 changes: 48 additions & 23 deletions examples/configurable/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ use std::time::Instant;

use crossbeam::channel::{unbounded, Receiver, Sender};
use eframe::{run_native, App, CreationContext};
use egui::{CollapsingHeader, Context, ScrollArea, Slider, Ui, Vec2};
use egui::{CollapsingHeader, Context, Pos2, ScrollArea, Slider, Ui};
use egui_graphs::events::Event;
use egui_graphs::{to_graph, Edge, Graph, GraphView, Node};
use egui_graphs::{to_graph, DefaultEdgeShape, DefaultNodeShape, Edge, Graph, GraphView, Node};
use fdg_sim::glam::Vec3;
use fdg_sim::{ForceGraph, ForceGraphHelper, Simulation, SimulationParameters};
use petgraph::stable_graph::{DefaultIx, EdgeIndex, NodeIndex, StableGraph};
Expand All @@ -27,8 +27,8 @@ pub struct ConfigurableApp {
settings_navigation: SettingsNavigation,
settings_style: SettingsStyle,

selected_nodes: Vec<Node<()>>,
selected_edges: Vec<Edge<()>>,
selected_nodes: Vec<Node<(), DefaultIx>>,
selected_edges: Vec<Edge<(), DefaultIx>>,
last_events: Vec<String>,

simulation_stopped: bool,
Expand Down Expand Up @@ -135,7 +135,7 @@ impl ConfigurableApp {
let sim_n = self.sim.get_graph_mut().node_weight_mut(*g_n_idx).unwrap();

let loc = sim_n.location;
g_n.set_location(Vec2::new(loc.x, loc.y));
g_n.set_location(Pos2::new(loc.x, loc.y));

if g_n.selected() {
self.selected_nodes.push(g_n.clone());
Expand Down Expand Up @@ -244,12 +244,14 @@ impl ConfigurableApp {

// location of new node is in surrounging of random existing node
let mut rng = rand::thread_rng();
let location = Vec2::new(
let location = Pos2::new(
random_n.location().x + 10. + rng.gen_range(0. ..50.),
random_n.location().y + 10. + rng.gen_range(0. ..50.),
);

let idx = self.g.g.add_node(Node::new(location, ()));
let idx = self.g.g.add_node(Node::new(()));
self.g.g[idx].bind(idx, location);

let n = self.g.g.node_weight_mut(idx).unwrap();
*n = n.with_label(format!("{:?}", idx));
let mut sim_node = fdg_sim::Node::new(idx.index().to_string().as_str(), ());
Expand Down Expand Up @@ -280,7 +282,9 @@ impl ConfigurableApp {
}

fn add_edge(&mut self, start: NodeIndex, end: NodeIndex) {
self.g.g.add_edge(start, end, Edge::new(()));
let idx = self.g.g.add_edge(start, end, Edge::new(()));
let order = self.g.g.edges_connecting(start, end).count();
self.g.g.edge_weight_mut(idx).unwrap().bind(idx, order);
self.sim.get_graph_mut().add_edge(start, end, 1.);
}

Expand All @@ -303,10 +307,32 @@ impl ConfigurableApp {
return;
}

let order = self.g.g.edge_weight(g_idx.unwrap()).unwrap().order();

self.g.g.remove_edge(g_idx.unwrap()).unwrap();

let sim_idx = self.sim.get_graph_mut().find_edge(start, end).unwrap();
self.sim.get_graph_mut().remove_edge(sim_idx).unwrap();

// update order of the edges
let left_siblings = self
.g
.g
.edges_connecting(start, end)
.map(|edge_ref| edge_ref.id())
.collect::<Vec<_>>();

left_siblings.iter().for_each(|idx| {
let sibling_order = self.g.g.edge_weight(*idx).unwrap().order();
if sibling_order < order {
return;
}
self.g
.g
.edge_weight_mut(*idx)
.unwrap()
.set_order(sibling_order - 1);
});
}

/// Removes all edges between two nodes
Expand Down Expand Up @@ -396,12 +422,6 @@ impl ConfigurableApp {
});

CollapsingHeader::new("Style").show(ui, |ui| {
ui.add(Slider::new(&mut self.settings_style.edge_radius_weight, 0. ..=5.)
.text("edge_radius_weight"));
ui.label("For every edge connected to node its radius is getting bigger by this value.");

ui.add_space(5.);

ui.checkbox(&mut self.settings_style.labels_always, "labels_always");
ui.label("Wheter to show labels always or when interacted only.");
});
Expand Down Expand Up @@ -551,25 +571,30 @@ impl App for ConfigurableApp {
egui::CentralPanel::default().show(ctx, |ui| {
let settings_interaction = &egui_graphs::SettingsInteraction::new()
.with_node_selection_enabled(self.settings_interaction.node_selection_enabled)
.with_node_selection_multi_enabled(self.settings_interaction.node_selection_multi_enabled)
.with_node_selection_multi_enabled(
self.settings_interaction.node_selection_multi_enabled,
)
.with_dragging_enabled(self.settings_interaction.dragging_enabled)
.with_node_clicking_enabled(self.settings_interaction.node_clicking_enabled)
.with_edge_clicking_enabled(self.settings_interaction.edge_clicking_enabled)
.with_edge_selection_enabled(self.settings_interaction.edge_selection_enabled)
.with_edge_selection_multi_enabled(self.settings_interaction.edge_selection_multi_enabled);
.with_edge_selection_multi_enabled(
self.settings_interaction.edge_selection_multi_enabled,
);
let settings_navigation = &egui_graphs::SettingsNavigation::new()
.with_zoom_and_pan_enabled(self.settings_navigation.zoom_and_pan_enabled)
.with_fit_to_screen_enabled(self.settings_navigation.fit_to_screen_enabled)
.with_zoom_speed(self.settings_navigation.zoom_speed);
let settings_style = &egui_graphs::SettingsStyle::new()
.with_labels_always(self.settings_style.labels_always)
.with_edge_radius_weight(self.settings_style.edge_radius_weight);
.with_labels_always(self.settings_style.labels_always);
ui.add(
&mut GraphView::new(&mut self.g)
.with_interactions(settings_interaction)
.with_navigations(settings_navigation)
.with_styles(settings_style)
.with_events(&self.event_publisher),
&mut GraphView::<_, _, _, _, DefaultNodeShape, DefaultEdgeShape<_>>::new(
&mut self.g,
)
.with_interactions(settings_interaction)
.with_navigations(settings_navigation)
.with_styles(settings_style)
.with_events(&self.event_publisher),
);
});

Expand Down
11 changes: 1 addition & 10 deletions examples/configurable/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,7 @@ impl Default for SettingsNavigation {
}
}

#[derive(Default)]
pub struct SettingsStyle {
pub edge_radius_weight: f32,
pub labels_always: bool,
}

impl Default for SettingsStyle {
fn default() -> Self {
Self {
edge_radius_weight: 1.,
labels_always: false,
}
}
}
96 changes: 14 additions & 82 deletions examples/custom_draw/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use eframe::{run_native, App, CreationContext};
use egui::{epaint::TextShape, Context, FontFamily, FontId, Rect, Rounding, Shape, Stroke, Vec2};
use egui_graphs::{default_edges_draw, Graph, GraphView, SettingsInteraction};
use egui::Context;
use egui_graphs::{DefaultEdgeShape, Graph, GraphView, SettingsInteraction, SettingsNavigation};
use node::NodeShape;
use petgraph::{
stable_graph::{DefaultIx, StableGraph},
Directed,
};

mod node;

pub struct CustomDrawApp {
g: Graph<(), (), Directed, DefaultIx>,
}
Expand All @@ -21,89 +24,18 @@ impl App for CustomDrawApp {
fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.add(
&mut GraphView::new(&mut self.g)
&mut GraphView::<_, _, _, _, NodeShape, DefaultEdgeShape<_>>::new(&mut self.g)
.with_navigations(
&SettingsNavigation::default()
.with_fit_to_screen_enabled(false)
.with_zoom_and_pan_enabled(true),
)
.with_interactions(
&SettingsInteraction::default()
.with_dragging_enabled(true)
.with_node_selection_enabled(true),
)
.with_custom_node_draw(|ctx, n, state, l| {
// lets draw a rect with label in the center for every node

// find node center location on the screen coordinates
let node_center_loc = n.screen_location(state.meta).to_pos2();

// find node radius accounting for current zoom level; we will use it as a reference for the rect and label sizes
let rad = n.screen_radius(state.meta, state.style);

// first create rect shape
let size = Vec2::new(rad * 1.5, rad * 1.5);
let rect = Rect::from_center_size(node_center_loc, size);
let shape_rect = Shape::rect_stroke(
rect,
Rounding::default(),
Stroke::new(1., n.color(ctx)),
);

// add rect to the layers
l.add(shape_rect);

// then create label
let color = ctx.style().visuals.text_color();
let galley = ctx.fonts(|f| {
f.layout_no_wrap(
n.label(),
FontId::new(rad, FontFamily::Monospace),
color,
)
});

// we need to offset label by half its size to place it in the center of the rect
let offset = Vec2::new(-galley.size().x / 2., -galley.size().y / 2.);

// create the shape and add it to the layers
let shape_label = TextShape::new(node_center_loc + offset, galley);
l.add(shape_label);
})
.with_custom_edge_draw(|ctx, bounds, edges, state, l| {
// draw edges with labels in the middle

// draw default edges
default_edges_draw(ctx, bounds, edges, state, l);

// get start and end nodes
let n_start = state.g.node(bounds.0).unwrap();
let n_end = state.g.node(bounds.1).unwrap();

// get start and end node locations
let loc_start = n_start.screen_location(state.meta);
let loc_end = n_end.screen_location(state.meta);

// compute edge center location
let center_loc = (loc_start + loc_end) / 2.;

// let label be the average of bound nodes sizes
let size = (n_start.screen_radius(state.meta, state.style)
+ n_end.screen_radius(state.meta, state.style))
/ 2.;

// create label
let color = ctx.style().visuals.text_color();
let galley = ctx.fonts(|f| {
f.layout_no_wrap(
format!("{}->{}", n_start.label(), n_end.label()),
FontId::new(size, FontFamily::Monospace),
color,
)
});

// we need to offset half the label size to place it in the center of the edge
let offset = Vec2::new(-galley.size().x / 2., -galley.size().y / 2.);

// create the shape and add it to the layers
let shape_label = TextShape::new((center_loc + offset).to_pos2(), galley);
l.add(shape_label);
}),
.with_node_selection_enabled(true)
.with_edge_selection_enabled(true),
),
);
});
}
Expand Down
Loading

0 comments on commit ddd68cc

Please sign in to comment.