diff --git a/Cargo.lock b/Cargo.lock index ed3d01d..1cc3a5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -865,7 +865,7 @@ dependencies = [ [[package]] name = "egui_graphs" -version = "0.14.0" +version = "0.15.0" dependencies = [ "crossbeam", "egui", diff --git a/Cargo.toml b/Cargo.toml index db5fb40..ffcf372 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "egui_graphs" -version = "0.14.0" +version = "0.15.0" authors = ["Dmitrii Samsonov "] license = "MIT" homepage = "https://github.com/blitzarx1/egui_graphs" diff --git a/README.md b/README.md index 221f698..d048ef3 100644 --- a/README.md +++ b/README.md @@ -16,6 +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` which demonstrates labels drawing for edges); ## Status The project is on track for a stable release v1.0.0. For the moment, breaking releases are still possible. diff --git a/examples/configurable/src/main.rs b/examples/configurable/src/main.rs index 3ce4c01..fdc7bb5 100644 --- a/examples/configurable/src/main.rs +++ b/examples/configurable/src/main.rs @@ -1,7 +1,6 @@ use std::time::Instant; use crossbeam::channel::{unbounded, Receiver, Sender}; -use eframe::glow::WAIT_FAILED; use eframe::{run_native, App, CreationContext}; use egui::{CollapsingHeader, Context, ScrollArea, Slider, Ui, Vec2}; use egui_graphs::events::Event; diff --git a/examples/custom_draw/README.md b/examples/custom_draw/README.md index ba229d9..a0fb7b7 100644 --- a/examples/custom_draw/README.md +++ b/examples/custom_draw/README.md @@ -1,5 +1,5 @@ # Custom draw -Basic example which demonstrates the usage of `GraphView` widget. +Example demonstrates how to use custom drawing functions for edge and nodes. Here we draw nodes as squares and adding edge labels for the standard edge shape. ## run ```bash diff --git a/examples/custom_draw/src/main.rs b/examples/custom_draw/src/main.rs index b8056e7..f69debd 100644 --- a/examples/custom_draw/src/main.rs +++ b/examples/custom_draw/src/main.rs @@ -1,8 +1,8 @@ use eframe::{run_native, App, CreationContext}; use egui::{ - epaint::TextShape, Context, FontFamily, FontId, Pos2, Rect, Rounding, Shape, Stroke, Vec2, + epaint::TextShape, Context, FontFamily, FontId, Rect, Rounding, Shape, Stroke, Vec2, }; -use egui_graphs::{Graph, GraphView}; +use egui_graphs::{default_edges_draw, Graph, GraphView, SettingsInteraction}; use petgraph::{stable_graph::StableGraph, Directed}; pub struct BasicApp { @@ -19,40 +19,91 @@ 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).with_custom_node_draw( - |ctx, n, meta, style, 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(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(meta, 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)), - ); - - // then create shape for the label placing it in the center of the rect - 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 a bit to place the label in the center of the rect - let label_loc = - Pos2::new(node_center_loc.x - rad / 2., node_center_loc.y - rad / 2.); - let shape_label = TextShape::new(label_loc, galley); - - // add shapes to the drawing layers; the drawing process is happening in the widget lifecycle. - l.add(shape_rect); - l.add(shape_label); - }, - )); + ui.add( + &mut GraphView::new(&mut self.g) + .with_interactions( + &SettingsInteraction::default() + .with_dragging_enabled(true) + .with_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); + }), + ); }); } } diff --git a/src/draw/custom.rs b/src/draw/custom.rs new file mode 100644 index 0000000..c2025da --- /dev/null +++ b/src/draw/custom.rs @@ -0,0 +1,36 @@ +use egui::Context; +use petgraph::{stable_graph::NodeIndex, EdgeType}; + +use crate::{Edge, Graph, Metadata, Node, SettingsStyle}; + +use super::Layers; + +/// Contains all the data about current widget state which is needed for custom drawing functions. +pub struct WidgetState<'a, N: Clone, E: Clone, Ty: EdgeType> { + pub g: &'a Graph, + pub style: &'a SettingsStyle, + pub meta: &'a Metadata, +} + +/// Allows to fully customize what shape would be drawn for node. +/// The function is called for every node in the graph. +/// +/// Parameters: +/// - egui context, is needed for computing node props and styles; +/// - node reference, contains all node data; +/// - widget state with references to graph, style and metadata; +/// - when you create a shape, add it to the layers. +pub type FnCustomNodeDraw = + fn(&Context, n: &Node, &WidgetState, &mut Layers); + +/// Allows to fully customize what shape would be drawn for an edge. +/// The function is **called once for every node pair** which has edges connecting them. So make sure you have drawn all the edges which are passed to the function. +/// +/// Parameters: +/// - egui context, is needed for computing node props and styles; +/// - start node index and end node index; +/// - vector of edges, all edges between start and end nodes; +/// - widget state with references to graph, style and metadata; +/// - when you create a shape, add it to the layers. +pub type FnCustomEdgeDraw = + fn(&Context, (NodeIndex, NodeIndex), Vec<&Edge>, &WidgetState, &mut Layers); diff --git a/src/draw/drawer.rs b/src/draw/drawer.rs index 8fb42f7..2a5bef0 100644 --- a/src/draw/drawer.rs +++ b/src/draw/drawer.rs @@ -1,26 +1,18 @@ -use std::{collections::HashMap, f32::consts::PI}; +use std::collections::HashMap; -use egui::{ - epaint::{CircleShape, CubicBezierShape, QuadraticBezierShape, TextShape}, - Color32, Context, FontFamily, FontId, Painter, Pos2, Shape, Stroke, Vec2, -}; +use egui::Painter; use petgraph::{stable_graph::NodeIndex, EdgeType}; -use crate::{settings::SettingsStyle, Edge, Graph, Metadata, Node}; +use crate::{settings::SettingsStyle, Edge, Graph, Metadata}; -use super::layers::Layers; +use super::{ + custom::{FnCustomEdgeDraw, FnCustomNodeDraw, WidgetState}, + default_edges_draw, default_node_draw, + layers::Layers, +}; /// Mapping for 2 nodes and all edges between them -type EdgeMap<'a, E> = HashMap<(NodeIndex, NodeIndex), Vec>>; - -/// Custom node draw function. Allows to fully customize what shaped would be drawn for node. -/// The function is called for every node in the graph. Parmaeters: -/// - `ctx` - egui context, is needed for computing node props and styles; -/// - `n` - node reference, contains all node data; -/// - `meta` - metadata, contains current zoom level and pan; -/// - `style` - style settings, contains all style settings; -/// - `l` - drawing layers, contains all shapes which will be drawn. When you create a shape, add it to the layers. -pub type FnCustomNodeDraw = fn(&Context, n: &Node, &Metadata, &SettingsStyle, &mut Layers); +type EdgeMap<'a, E> = HashMap<(NodeIndex, NodeIndex), Vec<&'a Edge>>; pub struct Drawer<'a, N: Clone, E: Clone, Ty: EdgeType> { p: Painter, @@ -29,7 +21,8 @@ pub struct Drawer<'a, N: Clone, E: Clone, Ty: EdgeType> { style: &'a SettingsStyle, meta: &'a Metadata, - custom_node_draw: Option>, + custom_node_draw: Option>, + custom_edge_draw: Option>, } impl<'a, N: Clone, E: Clone, Ty: EdgeType> Drawer<'a, N, E, Ty> { @@ -38,7 +31,8 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType> Drawer<'a, N, E, Ty> { g: &'a Graph, style: &'a SettingsStyle, meta: &'a Metadata, - custom_node_draw: Option>, + custom_node_draw: Option>, + custom_edge_draw: Option>, ) -> Self { Drawer { g, @@ -46,6 +40,7 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType> Drawer<'a, N, E, Ty> { style, meta, custom_node_draw, + custom_edge_draw, } } @@ -59,11 +54,16 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType> Drawer<'a, N, E, Ty> { } fn fill_layers_nodes(&self, l: &mut Layers) { + let state = &WidgetState { + g: self.g, + meta: self.meta, + style: self.style, + }; self.g .nodes_iter() .for_each(|(_, n)| match self.custom_node_draw { - Some(f) => f(self.p.ctx(), n, self.meta, self.style, l), - None => self.default_node_draw(self.p.ctx(), n, self.meta, self.style, l), + Some(f) => f(self.p.ctx(), n, state, l), + None => default_node_draw(self.p.ctx(), n, state, l), }); } @@ -73,227 +73,20 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType> Drawer<'a, N, E, Ty> { self.g.edges_iter().for_each(|(idx, e)| { let (source, target) = self.g.edge_endpoints(idx).unwrap(); // compute map with edges between 2 nodes - edge_map - .entry((source, target)) - .or_insert_with(Vec::new) - .push(e.clone()); - }); - - edge_map.iter().for_each(|((start, end), edges)| { - let mut order = edges.len(); - edges.iter().for_each(|e| { - order -= 1; - - if start == end { - self.draw_edge_looped(l, start, e, order); - } else { - self.draw_edge_basic(l, start, end, e, order); - } - }); + edge_map.entry((source, target)).or_default().push(e); }); - } - - fn draw_edge_looped(&self, l: &mut Layers, n_idx: &NodeIndex, e: &Edge, order: usize) { - let node = self.g.node(*n_idx).unwrap(); - - let rad = node.screen_radius(self.meta, self.style); - let center = node.screen_location(self.meta); - let center_horizon_angle = PI / 4.; - let y_intersect = center.y - rad * center_horizon_angle.sin(); - - let edge_start = Pos2::new(center.x - rad * center_horizon_angle.cos(), y_intersect); - let edge_end = Pos2::new(center.x + rad * center_horizon_angle.cos(), y_intersect); - - let loop_size = rad * (self.style.edge_looped_size + order as f32); - - let control_point1 = Pos2::new(center.x + loop_size, center.y - loop_size); - let control_point2 = Pos2::new(center.x - loop_size, center.y - loop_size); - - let stroke = Stroke::new(e.width() * self.meta.zoom, e.color(self.p.ctx())); - let shape = CubicBezierShape::from_points_stroke( - [edge_end, control_point1, control_point2, edge_start], - false, - Color32::TRANSPARENT, - stroke, - ); - l.add(shape); - } - - fn draw_edge_basic( - &self, - l: &mut Layers, - start_idx: &NodeIndex, - end_idx: &NodeIndex, - e: &Edge, - order: usize, - ) { - let n_start = self.g.node(*start_idx).unwrap(); - let n_end = self.g.node(*end_idx).unwrap(); - - let loc_start = n_start.screen_location(self.meta).to_pos2(); - let loc_end = n_end.screen_location(self.meta).to_pos2(); - let rad_start = n_start.screen_radius(self.meta, self.style); - let rad_end = n_end.screen_radius(self.meta, self.style); - - let vec = loc_end - loc_start; - let dist: f32 = vec.length(); - let dir = vec / dist; - - let start_node_radius_vec = Vec2::new(rad_start, rad_start) * dir; - let end_node_radius_vec = Vec2::new(rad_end, rad_end) * dir; - - let tip_end = loc_start + vec - end_node_radius_vec; - - let edge_start = loc_start + start_node_radius_vec; - let edge_end = match self.g.is_directed() { - true => tip_end - e.tip_size() * self.meta.zoom * dir, - false => tip_end, + let state = &WidgetState { + g: self.g, + meta: self.meta, + style: self.style, }; - let color = e.color(self.p.ctx()); - let stroke_edge = Stroke::new(e.width() * self.meta.zoom, color); - let stroke_tip = Stroke::new(0., color); - - // draw straight edge - if order == 0 { - let tip_start_1 = - tip_end - e.tip_size() * self.meta.zoom * rotate_vector(dir, e.tip_angle()); - let tip_start_2 = - tip_end - e.tip_size() * self.meta.zoom * rotate_vector(dir, -e.tip_angle()); - - let shape = Shape::line_segment([edge_start, edge_end], stroke_edge); - l.add(shape); - - // draw tips for directed edges - if self.g.is_directed() { - let shape_tip = Shape::convex_polygon( - vec![tip_end, tip_start_1, tip_start_2], - color, - stroke_tip, - ); - l.add(shape_tip); - } - - return; - } - - // draw curved edge - let dir_perpendicular = Vec2::new(-dir.y, dir.x); - let center_point = (edge_start + edge_end.to_vec2()).to_vec2() / 2.0; - let control_point = (center_point - + dir_perpendicular * e.curve_size() * self.meta.zoom * order as f32) - .to_pos2(); - - let tip_vec = control_point - tip_end; - let tip_dir = tip_vec / tip_vec.length(); - let tip_size = e.tip_size() * self.meta.zoom; - - let arrow_tip_dir_1 = rotate_vector(tip_dir, e.tip_angle()) * tip_size; - let arrow_tip_dir_2 = rotate_vector(tip_dir, -e.tip_angle()) * tip_size; - - let tip_start_1 = tip_end + arrow_tip_dir_1; - let tip_start_2 = tip_end + arrow_tip_dir_2; - - let edge_end_curved = point_between(tip_start_1, tip_start_2); - - // draw curved not selected - let shape_curved = QuadraticBezierShape::from_points_stroke( - [edge_start, control_point, edge_end_curved], - false, - Color32::TRANSPARENT, - stroke_edge, - ); - l.add(shape_curved); - - let shape_tip_curved = - Shape::convex_polygon(vec![tip_end, tip_start_1, tip_start_2], color, stroke_tip); - l.add(shape_tip_curved); - } - - fn default_node_draw( - &self, - ctx: &Context, - n: &Node, - m: &Metadata, - style: &SettingsStyle, - l: &mut Layers, - ) { - let is_interacted = n.selected() || n.dragged(); - let loc = n.screen_location(m).to_pos2(); - let rad = match is_interacted { - true => n.screen_radius(m, self.style) * 1.5, - false => n.screen_radius(m, self.style), - }; - - let color = n.color(ctx); - let shape_node = CircleShape { - center: loc, - radius: rad, - fill: color, - stroke: Stroke::new(1., color), - }; - match is_interacted { - true => l.add_top(shape_node), - false => l.add(shape_node), - }; - - let show_label = style.labels_always || is_interacted; - if !show_label { - return; - }; - - let color = ctx.style().visuals.text_color(); - let label_pos = Pos2::new(loc.x, loc.y - rad * 2.); - let label_size = rad; - let galley = ctx.fonts(|f| { - f.layout_no_wrap( - n.label(), - FontId::new(label_size, FontFamily::Monospace), - color, - ) - }); - - let shape_label = TextShape::new(label_pos, galley); - match is_interacted { - true => l.add_top(shape_label), - false => l.add(shape_label), - }; - } -} - -/// rotates vector by angle -fn rotate_vector(vec: Vec2, angle: f32) -> Vec2 { - let cos = angle.cos(); - let sin = angle.sin(); - Vec2::new(cos * vec.x - sin * vec.y, sin * vec.x + cos * vec.y) -} - -/// finds point exactly in the middle between 2 points -fn point_between(p1: Pos2, p2: Pos2) -> Pos2 { - let base = p1 - p2; - let base_len = base.length(); - let dir = base / base_len; - p1 - (base_len / 2.) * dir -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_rotate_vector() { - let vec = Vec2::new(1.0, 0.0); - let angle = PI / 2.0; - let rotated = rotate_vector(vec, angle); - assert!((rotated.x - 0.0).abs() < 1e-6); - assert!((rotated.y - 1.0).abs() < 1e-6); - } - - #[test] - fn test_point_between() { - let m = point_between(Pos2::new(0.0, 0.0), Pos2::new(2.0, 0.0)); - assert!((m.x - 1.0).abs() < 1e-6); - assert!((m.y).abs() < 1e-6); + edge_map + .into_iter() + .for_each(|((start, end), edges)| match self.custom_edge_draw { + Some(f) => f(self.p.ctx(), (start, end), edges, state, l), + None => default_edges_draw(self.p.ctx(), (start, end), edges, state, l), + }); } } diff --git a/src/draw/edge.rs b/src/draw/edge.rs new file mode 100644 index 0000000..f46d9ab --- /dev/null +++ b/src/draw/edge.rs @@ -0,0 +1,188 @@ +use std::f32::consts::PI; + +use egui::{ + epaint::{CubicBezierShape, QuadraticBezierShape}, + Color32, Context, Pos2, Shape, Stroke, Vec2, +}; +use petgraph::{stable_graph::NodeIndex, EdgeType}; + +use crate::{Edge, Node}; + +use super::{custom::WidgetState, Layers}; + +pub fn default_edges_draw( + ctx: &Context, + bounds: (NodeIndex, NodeIndex), + edges: Vec<&Edge>, + state: &WidgetState, + l: &mut Layers, +) { + let (idx_start, idx_end) = bounds; + let mut order = edges.len(); + edges.iter().for_each(|e| { + let n_start = state.g.node(idx_start).unwrap(); + let n_end = state.g.node(idx_end).unwrap(); + + order -= 1; + + if idx_start == idx_end { + draw_edge_looped(ctx, l, n_start, e, order, state); + } else { + draw_edge_basic(ctx, l, n_start, n_end, e, order, state); + } + }); +} + +fn draw_edge_basic( + ctx: &Context, + l: &mut Layers, + n_start: &Node, + n_end: &Node, + e: &Edge, + order: usize, + state: &WidgetState, +) { + let loc_start = n_start.screen_location(state.meta).to_pos2(); + let loc_end = n_end.screen_location(state.meta).to_pos2(); + let rad_start = n_start.screen_radius(state.meta, state.style); + let rad_end = n_end.screen_radius(state.meta, state.style); + + let vec = loc_end - loc_start; + let dist: f32 = vec.length(); + let dir = vec / dist; + + let start_node_radius_vec = Vec2::new(rad_start, rad_start) * dir; + let end_node_radius_vec = Vec2::new(rad_end, rad_end) * dir; + + let tip_end = loc_start + vec - end_node_radius_vec; + + let edge_start = loc_start + start_node_radius_vec; + let edge_end = match state.g.is_directed() { + true => tip_end - e.tip_size() * state.meta.zoom * dir, + false => tip_end, + }; + + let color = e.color(ctx); + let stroke_edge = Stroke::new(e.width() * state.meta.zoom, color); + let stroke_tip = Stroke::new(0., color); + + // draw straight edge + if order == 0 { + let tip_start_1 = + tip_end - e.tip_size() * state.meta.zoom * rotate_vector(dir, e.tip_angle()); + let tip_start_2 = + tip_end - e.tip_size() * state.meta.zoom * rotate_vector(dir, -e.tip_angle()); + + let shape = Shape::line_segment([edge_start, edge_end], stroke_edge); + l.add(shape); + + // draw tips for directed edges + if state.g.is_directed() { + let shape_tip = + Shape::convex_polygon(vec![tip_end, tip_start_1, tip_start_2], color, stroke_tip); + l.add(shape_tip); + } + + return; + } + + // draw curved edge + let dir_perpendicular = Vec2::new(-dir.y, dir.x); + let center_point = (edge_start + edge_end.to_vec2()).to_vec2() / 2.0; + let control_point = (center_point + + dir_perpendicular * e.curve_size() * state.meta.zoom * order as f32) + .to_pos2(); + + let tip_vec = control_point - tip_end; + let tip_dir = tip_vec / tip_vec.length(); + let tip_size = e.tip_size() * state.meta.zoom; + + let arrow_tip_dir_1 = rotate_vector(tip_dir, e.tip_angle()) * tip_size; + let arrow_tip_dir_2 = rotate_vector(tip_dir, -e.tip_angle()) * tip_size; + + let tip_start_1 = tip_end + arrow_tip_dir_1; + let tip_start_2 = tip_end + arrow_tip_dir_2; + + let edge_end_curved = point_between(tip_start_1, tip_start_2); + + // draw curved not selected + let shape_curved = QuadraticBezierShape::from_points_stroke( + [edge_start, control_point, edge_end_curved], + false, + Color32::TRANSPARENT, + stroke_edge, + ); + l.add(shape_curved); + + let shape_tip_curved = + Shape::convex_polygon(vec![tip_end, tip_start_1, tip_start_2], color, stroke_tip); + l.add(shape_tip_curved); +} + +fn draw_edge_looped( + ctx: &Context, + l: &mut Layers, + node: &Node, + e: &Edge, + order: usize, + state: &WidgetState, +) { + let rad = node.screen_radius(state.meta, state.style); + let center = node.screen_location(state.meta); + let center_horizon_angle = PI / 4.; + let y_intersect = center.y - rad * center_horizon_angle.sin(); + + let edge_start = Pos2::new(center.x - rad * center_horizon_angle.cos(), y_intersect); + let edge_end = Pos2::new(center.x + rad * center_horizon_angle.cos(), y_intersect); + + let loop_size = rad * (state.style.edge_looped_size + order as f32); + + let control_point1 = Pos2::new(center.x + loop_size, center.y - loop_size); + let control_point2 = Pos2::new(center.x - loop_size, center.y - loop_size); + + let stroke = Stroke::new(e.width() * state.meta.zoom, e.color(ctx)); + let shape = CubicBezierShape::from_points_stroke( + [edge_end, control_point1, control_point2, edge_start], + false, + Color32::TRANSPARENT, + stroke, + ); + + l.add(shape); +} + +/// rotates vector by angle +fn rotate_vector(vec: Vec2, angle: f32) -> Vec2 { + let cos = angle.cos(); + let sin = angle.sin(); + Vec2::new(cos * vec.x - sin * vec.y, sin * vec.x + cos * vec.y) +} + +/// finds point exactly in the middle between 2 points +fn point_between(p1: Pos2, p2: Pos2) -> Pos2 { + let base = p1 - p2; + let base_len = base.length(); + let dir = base / base_len; + p1 - (base_len / 2.) * dir +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_rotate_vector() { + let vec = Vec2::new(1.0, 0.0); + let angle = PI / 2.0; + let rotated = rotate_vector(vec, angle); + assert!((rotated.x - 0.0).abs() < 1e-6); + assert!((rotated.y - 1.0).abs() < 1e-6); + } + + #[test] + fn test_point_between() { + let m = point_between(Pos2::new(0.0, 0.0), Pos2::new(2.0, 0.0)); + assert!((m.x - 1.0).abs() < 1e-6); + assert!((m.y).abs() < 1e-6); + } +} diff --git a/src/draw/mod.rs b/src/draw/mod.rs index c049bd5..b1c1997 100644 --- a/src/draw/mod.rs +++ b/src/draw/mod.rs @@ -1,5 +1,11 @@ +mod custom; mod drawer; +mod edge; mod layers; +mod node; -pub use self::drawer::{Drawer, FnCustomNodeDraw}; +pub use self::custom::{FnCustomEdgeDraw, FnCustomNodeDraw}; +pub use self::drawer::Drawer; +pub use self::edge::default_edges_draw; pub use self::layers::Layers; +pub use self::node::default_node_draw; diff --git a/src/draw/node.rs b/src/draw/node.rs new file mode 100644 index 0000000..e7f7047 --- /dev/null +++ b/src/draw/node.rs @@ -0,0 +1,57 @@ +use egui::{ + epaint::{CircleShape, TextShape}, + Context, FontFamily, FontId, Pos2, Stroke, +}; +use petgraph::EdgeType; + +use crate::Node; + +use super::{custom::WidgetState, Layers}; + +pub fn default_node_draw( + ctx: &Context, + n: &Node, + state: &WidgetState, + l: &mut Layers, +) { + let is_interacted = n.selected() || n.dragged(); + let loc = n.screen_location(state.meta).to_pos2(); + let rad = match is_interacted { + true => n.screen_radius(state.meta, state.style) * 1.5, + false => n.screen_radius(state.meta, state.style), + }; + + let color = n.color(ctx); + let shape_node = CircleShape { + center: loc, + radius: rad, + fill: color, + stroke: Stroke::new(1., color), + }; + match is_interacted { + true => l.add_top(shape_node), + false => l.add(shape_node), + }; + + let show_label = state.style.labels_always || is_interacted; + if !show_label { + return; + }; + + let color = ctx.style().visuals.text_color(); + let label_pos = Pos2::new(loc.x, loc.y - rad * 2.); + let label_size = rad; + let galley = ctx.fonts(|f| { + f.layout_no_wrap( + n.label(), + FontId::new(label_size, FontFamily::Monospace), + color, + ) + }); + + let shape_label = TextShape::new(label_pos, galley); + match is_interacted { + true => l.add_top(shape_label), + false => l.add(shape_label), + }; +} diff --git a/src/graph_view.rs b/src/graph_view.rs index a07e4e6..bdd30e2 100644 --- a/src/graph_view.rs +++ b/src/graph_view.rs @@ -5,8 +5,7 @@ use crate::events::{ }; use crate::{ computed::ComputedState, - draw::Drawer, - draw::FnCustomNodeDraw, + draw::{Drawer, FnCustomEdgeDraw, FnCustomNodeDraw}, metadata::Metadata, settings::SettingsNavigation, settings::{SettingsInteraction, SettingsStyle}, @@ -37,7 +36,9 @@ pub struct GraphView<'a, N: Clone, E: Clone, Ty: EdgeType> { settings_navigation: SettingsNavigation, settings_style: SettingsStyle, g: &'a mut Graph, - custom_node_draw: Option>, + + custom_edge_draw: Option>, + custom_node_draw: Option>, #[cfg(feature = "events")] events_publisher: Option<&'a Sender>, @@ -62,6 +63,7 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType> Widget for &mut GraphView<'a, N, E, T &self.settings_style, &meta, self.custom_node_draw, + self.custom_edge_draw, ) .draw(); @@ -82,18 +84,27 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType> GraphView<'a, N, E, Ty> { settings_style: Default::default(), settings_interaction: Default::default(), settings_navigation: Default::default(), + custom_node_draw: Default::default(), + custom_edge_draw: Default::default(), #[cfg(feature = "events")] events_publisher: Default::default(), } } - pub fn with_custom_node_draw(mut self, func: FnCustomNodeDraw) -> Self { + /// Sets a function that will be called instead of the default drawer for every node to draw custom shapes. + pub fn with_custom_node_draw(mut self, func: FnCustomNodeDraw) -> Self { self.custom_node_draw = Some(func); self } + /// Sets a function that will be called instead of the default drawer for every pair of nodes connected with edges to draw custom shapes. + pub fn with_custom_edge_draw(mut self, func: FnCustomEdgeDraw) -> Self { + self.custom_edge_draw = Some(func); + self + } + /// Makes widget interactive according to the provided settings. pub fn with_interactions(mut self, settings_interaction: &SettingsInteraction) -> Self { self.settings_interaction = settings_interaction.clone(); diff --git a/src/lib.rs b/src/lib.rs index 468a279..709869b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ mod computed; +mod draw; mod elements; mod graph; mod graph_view; @@ -6,9 +7,8 @@ mod metadata; mod settings; mod transform; -pub mod draw; - pub use self::computed::ComputedNode; +pub use self::draw::{default_edges_draw, default_node_draw, FnCustomEdgeDraw, FnCustomNodeDraw}; pub use self::elements::{Edge, Node}; pub use self::graph::Graph; pub use self::graph_view::GraphView;