From db80061a872607d1f0458062583ee69131b3a56d Mon Sep 17 00:00:00 2001 From: starlord Date: Thu, 19 Oct 2023 13:59:18 +0400 Subject: [PATCH] Refactor changes (#94) * Replace changes with events * Add feature flag for the events functionality * Small bug fixes and `configurable` example enhancements --- Cargo.lock | 31 +++- Cargo.toml | 8 +- README.md | 27 +-- examples/configurable/Cargo.toml | 3 +- examples/configurable/src/main.rs | 263 ++++++++++++++---------------- examples/custom_draw/Cargo.toml | 2 +- examples/custom_draw/src/main.rs | 4 +- examples/undirected/src/main.rs | 4 +- src/change.rs | 72 -------- src/draw/drawer.rs | 10 +- src/elements/node.rs | 6 +- src/elements/node_style.rs | 2 +- src/events/event.rs | 60 +++++++ src/events/mod.rs | 6 + src/graph.rs | 4 +- src/graph_view.rs | 145 ++++++++++------ src/lib.rs | 5 +- 17 files changed, 354 insertions(+), 298 deletions(-) delete mode 100644 src/change.rs create mode 100644 src/events/event.rs create mode 100644 src/events/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 956785d..319398d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -572,6 +572,7 @@ dependencies = [ "fdg-sim", "petgraph", "rand", + "serde_json", ] [[package]] @@ -847,13 +848,14 @@ dependencies = [ [[package]] name = "egui_graphs" -version = "0.13.3" +version = "0.14.0" dependencies = [ "crossbeam", "egui", "petgraph", "rand", "serde", + "serde_json", ] [[package]] @@ -1332,6 +1334,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + [[package]] name = "jni" version = "0.21.1" @@ -1877,9 +1885,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] @@ -2000,6 +2008,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + [[package]] name = "same-file" version = "1.0.6" @@ -2054,6 +2068,17 @@ dependencies = [ "syn 2.0.32", ] +[[package]] +name = "serde_json" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_repr" version = "0.1.15" diff --git a/Cargo.toml b/Cargo.toml index 19b9026..db5fb40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "egui_graphs" -version = "0.13.3" +version = "0.14.0" authors = ["Dmitrii Samsonov "] license = "MIT" homepage = "https://github.com/blitzarx1/egui_graphs" @@ -12,11 +12,13 @@ edition = "2021" egui = "0.23" rand = "0.8" petgraph = "0.6" -crossbeam = "0.8" -serde = {version = "1.0", optional = true} +crossbeam = { version = "0.8", optional = true } +serde = { version = "1.0", features = ["derive"], optional = true } +serde_json = { version = "1.0", optional = true } [features] egui_persistence = ["dep:serde"] +events = ["dep:serde", "dep:serde_json", "dep:crossbeam"] [workspace] members = ["examples/*"] diff --git a/README.md b/README.md index 3849dca..221f698 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,25 @@ The project implements a Widget for the egui framework, enabling easy visualizat - [x] Node labels; - [x] Node interactions and events reporting: click, double click, select, drag; - [x] Style configuration via egui context styles; -- [x] egui dark/light theme support; +- [x] Dark/Light theme support via egui context styles; +- [x] Events reporting to extend the graph functionality by the user handling them; ## Status The project is on track for a stable release v1.0.0. For the moment, breaking releases are still possible. -## Egui features support +## Features +### Events +Can be enabled with `events` feature. +- [x] Node click; +- [x] Node double click; +- [x] Node select; +- [x] Node move; +- [x] Node drag; +- [ ] Node hover; + +Combining this feature with custom node draw function allows to implement custom node behavior and drawing according to the events happening. + +## Egui crates features support ### Persistence To use egui `persistence` feature you need to enable `egui_persistence` feature of this crate. For example: ```toml @@ -30,7 +43,6 @@ egui = {version="0.23", features = ["persistence"]} ## Examples ### Basic setup example #### Step 1: Setting up the BasicApp struct. - First, let's define the `BasicApp` struct that will hold the graph. ```rust pub struct BasicApp { @@ -39,7 +51,6 @@ pub struct BasicApp { ``` #### Step 2: Implementing the new() function. - Next, implement the `new()` function for the `BasicApp` struct. ```rust impl BasicApp { @@ -51,7 +62,6 @@ impl BasicApp { ``` #### Step 3: Generating the graph. - Create a helper function called `generate_graph()`. In this example, we create three nodes with and three edges connecting them in a triangular pattern. ```rust fn generate_graph() -> StableGraph<(), (), Directed> { @@ -70,7 +80,6 @@ fn generate_graph() -> StableGraph<(), (), Directed> { ``` #### Step 4: Implementing the update() function. - Now, lets implement the `update()` function for the `BasicApp`. This function creates a `GraphView` widget providing a mutable reference to the graph, and adds it to `egui::CentralPanel` using the `ui.add()` function for adding widgets. ```rust impl App for BasicApp { @@ -83,7 +92,6 @@ impl App for BasicApp { ``` #### Step 5: Running the application. - Finally, run the application using the `run_native()` function with the specified native options and the `BasicApp`. ```rust fn main() { @@ -98,9 +106,4 @@ fn main() { ``` ![Screenshot 2023-10-14 at 23 49 49](https://github.com/blitzarx1/egui_graphs/assets/32969427/584b78de-bca3-421b-b003-9321fd3e1b13) - You can further customize the appearance and behavior of your graph by modifying the settings or adding more nodes and edges as needed. - -### Docs -Docs can be found [here](https://docs.rs/egui_graphs/latest/egui_graphs/) - diff --git a/examples/configurable/Cargo.toml b/examples/configurable/Cargo.toml index 0e8eaa0..3bc0693 100644 --- a/examples/configurable/Cargo.toml +++ b/examples/configurable/Cargo.toml @@ -6,9 +6,10 @@ license = "MIT" edition = "2021" [dependencies] -egui_graphs = { path = "../../" } +egui_graphs = { path = "../../", features = ["events"] } egui = "0.23" egui_plot = "0.23" +serde_json = "1.0" eframe = "0.23" petgraph = "0.6" fdg-sim = "0.9" diff --git a/examples/configurable/src/main.rs b/examples/configurable/src/main.rs index 246018c..3ce4c01 100644 --- a/examples/configurable/src/main.rs +++ b/examples/configurable/src/main.rs @@ -1,10 +1,11 @@ use std::time::Instant; use crossbeam::channel::{unbounded, Receiver, Sender}; +use eframe::glow::WAIT_FAILED; use eframe::{run_native, App, CreationContext}; -use egui::{CollapsingHeader, Color32, Context, ScrollArea, Slider, Ui, Vec2}; -use egui_graphs::{to_graph, Change, Edge, Graph, GraphView, Node}; -use egui_plot::{Line, Plot, PlotPoints}; +use egui::{CollapsingHeader, Context, ScrollArea, Slider, Ui, Vec2}; +use egui_graphs::events::Event; +use egui_graphs::{to_graph, Edge, Graph, GraphView, Node}; use fdg_sim::glam::Vec3; use fdg_sim::{ForceGraph, ForceGraphHelper, Simulation, SimulationParameters}; use petgraph::stable_graph::{EdgeIndex, NodeIndex, StableGraph}; @@ -16,8 +17,7 @@ use settings::{SettingsGraph, SettingsInteraction, SettingsNavigation, SettingsS mod settings; const SIMULATION_DT: f32 = 0.035; -const FPS_LINE_COLOR: Color32 = Color32::from_rgb(128, 128, 128); -const CHANGES_LIMIT: usize = 100; +const EVENTS_LIMIT: usize = 100; pub struct ConfigurableApp { g: Graph<(), (), Directed>, @@ -30,30 +30,32 @@ pub struct ConfigurableApp { selected_nodes: Vec>, selected_edges: Vec>, - last_changes: Vec, + last_events: Vec, simulation_stopped: bool, fps: f64, - fps_history: Vec, last_update_time: Instant, frames_last_time_span: usize, - changes_receiver: Receiver, - changes_sender: Sender, + event_publisher: Sender, + event_consumer: Receiver, + + pan: Option<[f32; 2]>, + zoom: Option, } impl ConfigurableApp { fn new(_: &CreationContext<'_>) -> Self { let settings_graph = SettingsGraph::default(); let (g, sim) = generate(&settings_graph); - let (changes_sender, changes_receiver) = unbounded(); + let (event_publisher, event_consumer) = unbounded(); Self { g, sim, - changes_receiver, - changes_sender, + event_consumer, + event_publisher, settings_graph, @@ -63,14 +65,16 @@ impl ConfigurableApp { selected_nodes: Default::default(), selected_edges: Default::default(), - last_changes: Default::default(), + last_events: Default::default(), simulation_stopped: false, fps: 0., - fps_history: Default::default(), last_update_time: Instant::now(), frames_last_time_span: 0, + + pan: Default::default(), + zoom: Default::default(), } } @@ -131,12 +135,6 @@ impl ConfigurableApp { let g_n = self.g.g.node_weight_mut(*g_n_idx).unwrap(); let sim_n = self.sim.get_graph_mut().node_weight_mut(*g_n_idx).unwrap(); - if g_n.dragged() { - let loc = g_n.location(); - sim_n.location = Vec3::new(loc.x, loc.y, 0.); - return; - } - let loc = sim_n.location; g_n.set_location(Vec2::new(loc.x, loc.y)); @@ -159,11 +157,6 @@ impl ConfigurableApp { self.last_update_time = now; self.fps = self.frames_last_time_span as f64 / elapsed.as_secs_f64(); self.frames_last_time_span = 0; - - self.fps_history.push(self.fps); - if self.fps_history.len() > 100 { - self.fps_history.remove(0); - } } } @@ -174,18 +167,46 @@ impl ConfigurableApp { self.g = g; self.sim = sim; self.settings_graph = settings_graph; - self.last_changes = Default::default(); + self.last_events = Default::default(); GraphView::<(), (), Directed>::reset_metadata(ui); } - fn handle_changes(&mut self) { - self.changes_receiver.try_iter().for_each(|ch| { - if self.last_changes.len() > CHANGES_LIMIT { - self.last_changes.remove(0); + fn handle_events(&mut self) { + self.event_consumer.try_iter().for_each(|e| { + if self.last_events.len() > EVENTS_LIMIT { + self.last_events.remove(0); } + self.last_events.push(serde_json::to_string(&e).unwrap()); + + match e { + Event::Pan(payload) => match self.pan { + Some(pan) => { + self.pan = Some([pan[0] + payload.diff[0], pan[1] + payload.diff[1]]); + } + None => { + self.pan = Some(payload.diff); + } + }, + Event::Zoom(z) => { + match self.zoom { + Some(zoom) => { + self.zoom = Some(zoom + z.diff); + } + None => { + self.zoom = Some(z.diff); + } + }; + } + Event::NodeMove(payload) => { + let node_id = NodeIndex::new(payload.id); + let diff = Vec3::new(payload.diff[0], payload.diff[1], 0.); - self.last_changes.push(ch); + let node = self.sim.get_graph_mut().node_weight_mut(node_id).unwrap(); + node.location += diff; + } + _ => {} + } }); } @@ -317,8 +338,8 @@ impl ConfigurableApp { }); } - fn draw_section_client(&mut self, ui: &mut Ui) { - CollapsingHeader::new("Client") + fn draw_section_app(&mut self, ui: &mut Ui) { + CollapsingHeader::new("App Config") .default_open(true) .show(ui, |ui| { ui.add_space(10.); @@ -347,7 +368,6 @@ impl ConfigurableApp { ui.add_space(10.); - ui.label("Style"); ui.separator(); }); } @@ -356,81 +376,72 @@ impl ConfigurableApp { CollapsingHeader::new("Widget") .default_open(true) .show(ui, |ui| { - ui.add_space(10.); - - ui.label("SettingsNavigation"); - ui.separator(); - - if ui - .checkbox(&mut self.settings_navigation.fit_to_screen_enabled, "fit_to_screen") - .changed() - && self.settings_navigation.fit_to_screen_enabled - { - self.settings_navigation.zoom_and_pan_enabled = false - }; - ui.label("Enable fit to screen to fit the graph to the screen on every frame."); - - ui.add_space(5.); - - ui.add_enabled_ui(!self.settings_navigation.fit_to_screen_enabled, |ui| { - ui.vertical(|ui| { - ui.checkbox(&mut self.settings_navigation.zoom_and_pan_enabled, "zoom_and_pan"); - ui.label("Zoom with ctrl + mouse wheel, pan with mouse drag."); - }).response.on_disabled_hover_text("disable fit_to_screen to enable zoom_and_pan"); - }); - - ui.add_space(10.); + CollapsingHeader::new("Navigation").default_open(true).show(ui, |ui|{ + if ui + .checkbox(&mut self.settings_navigation.fit_to_screen_enabled, "fit_to_screen") + .changed() + && self.settings_navigation.fit_to_screen_enabled + { + self.settings_navigation.zoom_and_pan_enabled = false + }; + ui.label("Enable fit to screen to fit the graph to the screen on every frame."); - ui.label("SettingsStyle"); - ui.separator(); + ui.add_space(5.); - 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_enabled_ui(!self.settings_navigation.fit_to_screen_enabled, |ui| { + ui.vertical(|ui| { + ui.checkbox(&mut self.settings_navigation.zoom_and_pan_enabled, "zoom_and_pan"); + ui.label("Zoom with ctrl + mouse wheel, pan with mouse drag."); + }).response.on_disabled_hover_text("disable fit_to_screen to enable zoom_and_pan"); + }); + }); - ui.add_space(5.); + 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.checkbox(&mut self.settings_style.labels_always, "labels_always"); - ui.label("Wheter to show labels always or when interacted only."); + ui.add_space(5.); - ui.add_space(10.); + ui.checkbox(&mut self.settings_style.labels_always, "labels_always"); + ui.label("Wheter to show labels always or when interacted only."); + }); - ui.label("SettingsInteraction"); - ui.separator(); + CollapsingHeader::new("Interaction").show(ui, |ui| { + ui.add_enabled_ui(!(self.settings_interaction.dragging_enabled || self.settings_interaction.selection_enabled || self.settings_interaction.selection_multi_enabled), |ui| { + ui.vertical(|ui| { + ui.checkbox(&mut self.settings_interaction.clicking_enabled, "clicking_enabled"); + ui.label("Check click events in last events"); + }).response.on_disabled_hover_text("node click is enabled when any of the interaction is also enabled"); + }); - ui.add_enabled_ui(!(self.settings_interaction.dragging_enabled || self.settings_interaction.selection_enabled || self.settings_interaction.selection_multi_enabled), |ui| { - ui.vertical(|ui| { - ui.checkbox(&mut self.settings_interaction.clicking_enabled, "clicking_enabled"); - ui.label("Check click events in last changes"); - }).response.on_disabled_hover_text("node click is enabled when any of the interaction is also enabled"); - }); + ui.add_space(5.); - ui.add_space(5.); + if ui.checkbox(&mut self.settings_interaction.dragging_enabled, "dragging_enabled").clicked() && self.settings_interaction.dragging_enabled { + self.settings_interaction.clicking_enabled = true; + }; + ui.label("To drag use LMB click + drag on a node."); - if ui.checkbox(&mut self.settings_interaction.dragging_enabled, "dragging_enabled").clicked() && self.settings_interaction.dragging_enabled { - self.settings_interaction.clicking_enabled = true; - }; - ui.label("To drag use LMB click + drag on a node."); + ui.add_space(5.); - ui.add_space(5.); + ui.add_enabled_ui(!self.settings_interaction.selection_multi_enabled, |ui| { + ui.vertical(|ui| { + if ui.checkbox(&mut self.settings_interaction.selection_enabled, "selection_enabled").clicked() && self.settings_interaction.selection_enabled { + self.settings_interaction.clicking_enabled = true; + }; + ui.label("Enable select to select nodes with LMB click. If node is selected clicking on it again will deselect it."); + }).response.on_disabled_hover_text("selection_multi_enabled enables select"); + }); - ui.add_enabled_ui(!self.settings_interaction.selection_multi_enabled, |ui| { - ui.vertical(|ui| { - if ui.checkbox(&mut self.settings_interaction.selection_enabled, "selection_enabled").clicked() && self.settings_interaction.selection_enabled { - self.settings_interaction.clicking_enabled = true; - }; - ui.label("Enable select to select nodes with LMB click. If node is selected clicking on it again will deselect it."); - }).response.on_disabled_hover_text("selection_multi_enabled enables select"); + if ui.checkbox(&mut self.settings_interaction.selection_multi_enabled, "selection_multi_enabled").changed() && self.settings_interaction.selection_multi_enabled { + self.settings_interaction.clicking_enabled = true; + self.settings_interaction.selection_enabled = true; + } + ui.label("Enable multiselect to select multiple nodes."); }); - if ui.checkbox(&mut self.settings_interaction.selection_multi_enabled, "selection_multi_enabled").changed() && self.settings_interaction.selection_multi_enabled { - self.settings_interaction.clicking_enabled = true; - self.settings_interaction.selection_enabled = true; - } - ui.label("Enable multiselect to select multiple nodes."); - - ui.collapsing("selected", |ui| { - ScrollArea::vertical().max_height(200.).show(ui, |ui| { + CollapsingHeader::new("Selected").default_open(true).show(ui, |ui| { + ScrollArea::vertical().auto_shrink([false, true]).max_height(200.).show(ui, |ui| { self.selected_nodes.iter().for_each(|node| { ui.label(format!("{:?}", node)); }); @@ -440,10 +451,10 @@ impl ConfigurableApp { }); }); - ui.collapsing("last changes", |ui| { - ScrollArea::vertical().max_height(200.).show(ui, |ui| { - self.last_changes.iter().rev().for_each(|node| { - ui.label(format!("{:?}", node)); + CollapsingHeader::new("Last Events").default_open(true).show(ui, |ui| { + ScrollArea::vertical().auto_shrink([false, true]).max_height(200.).show(ui, |ui| { + self.last_events.iter().rev().for_each(|event| { + ui.label(event); }); }); }); @@ -452,41 +463,19 @@ impl ConfigurableApp { fn draw_section_debug(&mut self, ui: &mut Ui) { CollapsingHeader::new("Debug") - .default_open(false) + .default_open(true) .show(ui, |ui| { - ui.add_space(10.); + if let Some(zoom) = self.zoom { + ui.label(format!("zoom: {:.5}", zoom)); + }; + if let Some(pan) = self.pan { + ui.label(format!("pan: [{:.5}, {:.5}]", pan[0], pan[1])); + }; - ui.vertical(|ui| { - ui.label(format!("fps: {:.1}", self.fps)); - ui.add_space(10.); - self.draw_fps(ui); - }); + ui.label(format!("FPS: {:.1}", self.fps)); }); } - fn draw_fps(&self, ui: &mut Ui) { - let points: PlotPoints = self - .fps_history - .iter() - .enumerate() - .map(|(i, val)| [i as f64, *val]) - .collect(); - - let line = Line::new(points).color(FPS_LINE_COLOR); - Plot::new("my_plot") - .min_size(Vec2::new(100., 80.)) - .show_x(false) - .show_y(false) - .show_background(false) - .show_axes([false, true]) - .allow_boxed_zoom(false) - .allow_double_click_reset(false) - .allow_drag(false) - .allow_scroll(false) - .allow_zoom(false) - .show(ui, |plot_ui| plot_ui.line(line)); - } - fn draw_counts_sliders(&mut self, ui: &mut Ui) { ui.horizontal(|ui| { let before = self.settings_graph.count_node as i32; @@ -526,15 +515,11 @@ impl App for ConfigurableApp { .min_width(250.) .show(ctx, |ui| { ScrollArea::vertical().show(ui, |ui| { - self.draw_section_client(ui); - + self.draw_section_app(ui); ui.add_space(10.); - - self.draw_section_widget(ui); - - ui.add_space(10.); - self.draw_section_debug(ui); + ui.add_space(10.); + self.draw_section_widget(ui); }); }); @@ -556,11 +541,11 @@ impl App for ConfigurableApp { .with_interactions(settings_interaction) .with_navigations(settings_navigation) .with_styles(settings_style) - .with_changes(&self.changes_sender), + .with_events(&self.event_publisher), ); }); - self.handle_changes(); + self.handle_events(); self.sync_graph_with_simulation(); self.update_simulation(); diff --git a/examples/custom_draw/Cargo.toml b/examples/custom_draw/Cargo.toml index d37381f..3825004 100644 --- a/examples/custom_draw/Cargo.toml +++ b/examples/custom_draw/Cargo.toml @@ -6,7 +6,7 @@ license = "MIT" edition = "2021" [dependencies] -egui_graphs = { path = "../../" } +egui_graphs = { path = "../../"} egui = "0.23" eframe = "0.23" petgraph = "0.6" diff --git a/examples/custom_draw/src/main.rs b/examples/custom_draw/src/main.rs index c037ed7..b8056e7 100644 --- a/examples/custom_draw/src/main.rs +++ b/examples/custom_draw/src/main.rs @@ -20,14 +20,14 @@ 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| { + |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); + let rad = n.screen_radius(meta, style); // first create rect shape let size = Vec2::new(rad * 1.5, rad * 1.5); diff --git a/examples/undirected/src/main.rs b/examples/undirected/src/main.rs index 4fb5c13..8aaef7d 100644 --- a/examples/undirected/src/main.rs +++ b/examples/undirected/src/main.rs @@ -13,9 +13,7 @@ pub struct BasicUndirectedApp { impl BasicUndirectedApp { fn new(_: &CreationContext<'_>) -> Self { let g = generate_graph(); - Self { - g: Graph::from(&g), - } + Self { g: Graph::from(&g) } } } diff --git a/src/change.rs b/src/change.rs deleted file mode 100644 index 0b5fbfd..0000000 --- a/src/change.rs +++ /dev/null @@ -1,72 +0,0 @@ -use egui::Vec2; -use petgraph::stable_graph::{EdgeIndex, NodeIndex}; - -/// `ChangeNode` is a enum that stores the changes to `Node` properties. -#[derive(Debug, Clone)] -pub enum ChangeNode { - /// Node has been clicked - Clicked { id: NodeIndex }, - - /// Node has been clicked - DoubleClicked { id: NodeIndex }, - - /// Node has changed its location - Location { id: NodeIndex, old: Vec2, new: Vec2 }, - - /// Node has been selected or deselected - Selected { id: NodeIndex, old: bool, new: bool }, - - /// Node is dragged or ceased to be dragged - Dragged { id: NodeIndex, old: bool, new: bool }, -} - -impl ChangeNode { - pub(crate) fn clicked(id: NodeIndex) -> Self { - Self::Clicked { id } - } - - pub(crate) fn double_clicked(id: NodeIndex) -> Self { - Self::DoubleClicked { id } - } - - pub(crate) fn change_location(id: NodeIndex, old: Vec2, new: Vec2) -> Self { - Self::Location { id, old, new } - } - - pub(crate) fn change_selected(id: NodeIndex, old: bool, new: bool) -> Self { - Self::Selected { id, old, new } - } - - pub(crate) fn change_dragged(id: NodeIndex, old: bool, new: bool) -> Self { - Self::Dragged { id, old, new } - } -} - -/// `ChangeEdge` is a enum that stores the changes to `Edge` properties. -#[derive(Debug, Clone)] -pub enum ChangeEdge { - Selected { id: EdgeIndex, old: bool, new: bool }, -} - -impl ChangeEdge { - pub(crate) fn change_selected(id: EdgeIndex, old: bool, new: bool) -> Self { - Self::Selected { id, old, new } - } -} - -/// Change is a enum that stores the changes to `Node` or `Edge` properties. -#[derive(Debug, Clone)] -pub enum Change { - Node(ChangeNode), - Edge(ChangeEdge), -} - -impl Change { - pub(crate) fn node(change: ChangeNode) -> Self { - Self::Node(change) - } - - pub(crate) fn edge(change: ChangeEdge) -> Self { - Self::Edge(change) - } -} diff --git a/src/draw/drawer.rs b/src/draw/drawer.rs index c4791d4..8fb42f7 100644 --- a/src/draw/drawer.rs +++ b/src/draw/drawer.rs @@ -96,7 +96,7 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType> Drawer<'a, N, E, Ty> { 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); + 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(); @@ -133,8 +133,8 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType> Drawer<'a, N, E, Ty> { 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); - let rad_end = n_end.screen_radius(self.meta); + 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(); @@ -222,8 +222,8 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType> Drawer<'a, N, E, Ty> { 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) * 1.5, - false => n.screen_radius(m), + true => n.screen_radius(m, self.style) * 1.5, + false => n.screen_radius(m, self.style), }; let color = n.color(ctx); diff --git a/src/elements/node.rs b/src/elements/node.rs index 7600e44..3776c8d 100644 --- a/src/elements/node.rs +++ b/src/elements/node.rs @@ -1,6 +1,6 @@ use egui::{Color32, Context, Vec2}; -use crate::{metadata::Metadata, ComputedNode}; +use crate::{metadata::Metadata, ComputedNode, SettingsStyle}; use super::StyleNode; @@ -40,8 +40,8 @@ impl Node { } /// Returns actual radius of the node on the screen. It accounts for the number of connections and current zoom value. - pub fn screen_radius(&self, m: &Metadata) -> f32 { - (self.radius() + self.num_connections() as f32) * m.zoom + pub fn screen_radius(&self, m: &Metadata, style: &SettingsStyle) -> f32 { + (self.radius() + self.num_connections() as f32 * style.edge_radius_weight) * m.zoom } pub fn radius(&self) -> f32 { diff --git a/src/elements/node_style.rs b/src/elements/node_style.rs index 1ea1953..3403365 100644 --- a/src/elements/node_style.rs +++ b/src/elements/node_style.rs @@ -7,4 +7,4 @@ impl Default for StyleNode { fn default() -> Self { Self { radius: 5. } } -} \ No newline at end of file +} diff --git a/src/events/event.rs b/src/events/event.rs new file mode 100644 index 0000000..f1d6ed8 --- /dev/null +++ b/src/events/event.rs @@ -0,0 +1,60 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct PayloadPan { + pub diff: [f32; 2], +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct PyaloadZoom { + pub diff: f32, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct PayloadNodeMove { + pub id: usize, + pub diff: [f32; 2], +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct PayloadNodeDragStart { + pub id: usize, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct PayloadNodeDragEnd { + pub id: usize, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct PayloadNodeSelect { + pub id: usize, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct PayloadNodeDeselect { + pub id: usize, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct PayloadNodeClick { + pub id: usize, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct PayloadNodeDoubleClick { + pub id: usize, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum Event { + Pan(PayloadPan), + Zoom(PyaloadZoom), + NodeMove(PayloadNodeMove), + NodeDragStart(PayloadNodeDragStart), + NodeDragEnd(PayloadNodeDragEnd), + NodeSelect(PayloadNodeSelect), + NodeDeselect(PayloadNodeDeselect), + NodeClick(PayloadNodeClick), + NodeDoubleClick(PayloadNodeDoubleClick), +} diff --git a/src/events/mod.rs b/src/events/mod.rs new file mode 100644 index 0000000..a2d83f2 --- /dev/null +++ b/src/events/mod.rs @@ -0,0 +1,6 @@ +mod event; + +pub use self::event::{ + Event, PayloadNodeClick, PayloadNodeDeselect, PayloadNodeDoubleClick, PayloadNodeDragEnd, + PayloadNodeDragStart, PayloadNodeMove, PayloadNodeSelect, PayloadPan, PyaloadZoom, +}; diff --git a/src/graph.rs b/src/graph.rs index 2f8657e..a4ecae2 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -29,13 +29,13 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType> Graph { pub fn node_by_screen_pos( &self, meta: &'a Metadata, - settings: &'a SettingsStyle, + style: &'a SettingsStyle, screen_pos: Pos2, ) -> Option<(NodeIndex, &Node)> { let pos_in_graph = (screen_pos.to_vec2() - meta.pan) / meta.zoom; self.nodes_iter().find(|(_, n)| { let dist_to_node = (n.location() - pos_in_graph).length(); - dist_to_node <= n.radius() + n.num_connections() as f32 * settings.edge_radius_weight + dist_to_node <= n.screen_radius(meta, style) / meta.zoom }) } diff --git a/src/graph_view.rs b/src/graph_view.rs index 4997765..a07e4e6 100644 --- a/src/graph_view.rs +++ b/src/graph_view.rs @@ -1,9 +1,9 @@ -use crossbeam::channel::Sender; -use egui::{Pos2, Rect, Response, Sense, Ui, Vec2, Widget}; -use petgraph::{stable_graph::NodeIndex, EdgeType}; - +#[cfg(feature = "events")] +use crate::events::{ + Event, PayloadNodeClick, PayloadNodeDeselect, PayloadNodeDoubleClick, PayloadNodeDragEnd, + PayloadNodeDragStart, PayloadNodeMove, PayloadNodeSelect, PayloadPan, PyaloadZoom, +}; use crate::{ - change::{Change, ChangeNode}, computed::ComputedState, draw::Drawer, draw::FnCustomNodeDraw, @@ -12,12 +12,16 @@ use crate::{ settings::{SettingsInteraction, SettingsStyle}, Graph, }; +#[cfg(feature = "events")] +use crossbeam::channel::Sender; +use egui::{Pos2, Rect, Response, Sense, Ui, Vec2, Widget}; +use petgraph::{stable_graph::NodeIndex, EdgeType}; /// Widget for visualizing and interacting with graphs. /// /// It implements [egui::Widget] and can be used like any other widget. /// -/// The widget uses a mutable reference to the [StableGraph, egui_graphs::Edge>] +/// The widget uses a mutable reference to the [petgraph::stable_graph::StableGraph, super::Edge>] /// struct to visualize and interact with the graph. `N` and `E` is arbitrary client data associated with nodes and edges. /// You can customize the visualization and interaction behavior using [SettingsInteraction], [SettingsNavigation] and [SettingsStyle] structs. /// @@ -33,9 +37,10 @@ pub struct GraphView<'a, N: Clone, E: Clone, Ty: EdgeType> { settings_navigation: SettingsNavigation, settings_style: SettingsStyle, g: &'a mut Graph, - changes_sender: Option<&'a Sender>, - custom_node_draw: Option>, + + #[cfg(feature = "events")] + events_publisher: Option<&'a Sender>, } impl<'a, N: Clone, E: Clone, Ty: EdgeType> Widget for &mut GraphView<'a, N, E, Ty> { @@ -77,8 +82,10 @@ 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(), - changes_sender: Default::default(), custom_node_draw: Default::default(), + + #[cfg(feature = "events")] + events_publisher: Default::default(), } } @@ -93,14 +100,6 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType> GraphView<'a, N, E, Ty> { self } - /// Make every interaction send [`Change`] to the provided [`crossbeam::channel::Sender`] as soon as interaction happens. - /// - /// Change events can be used to handle interactions on the application side. - pub fn with_changes(mut self, changes_sender: &'a Sender) -> Self { - self.changes_sender = Some(changes_sender); - self - } - /// Modifies default behaviour of navigation settings. pub fn with_navigations(mut self, settings_navigation: &SettingsNavigation) -> Self { self.settings_navigation = settings_navigation.clone(); @@ -118,6 +117,12 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType> GraphView<'a, N, E, Ty> { Metadata::default().store_into_ui(ui); } + #[cfg(feature = "events")] + pub fn with_events(mut self, events_publisher: &'a Sender) -> Self { + self.events_publisher = Some(events_publisher); + self + } + fn compute_state(&mut self) -> ComputedState { let mut computed = ComputedState::default(); @@ -209,7 +214,7 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType> GraphView<'a, N, E, Ty> { let n = self.g.node(idx).unwrap(); if n.selected() { - self.select_node(idx); + self.deselect_node(idx); return; } @@ -230,7 +235,7 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType> GraphView<'a, N, E, Ty> { self.g .node_by_screen_pos(meta, &self.settings_style, resp.hover_pos().unwrap()) { - self.set_dragged(idx, true); + self.set_drag_start(idx); } } @@ -245,7 +250,7 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType> GraphView<'a, N, E, Ty> { if resp.drag_released() && comp.dragged.is_some() { let n_idx = comp.dragged.unwrap(); - self.set_dragged(n_idx, false); + self.set_drag_end(n_idx); } } @@ -281,7 +286,8 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType> GraphView<'a, N, E, Ty> { let graph_center = (bounds.min.to_vec2() + bounds.max.to_vec2()) / 2.0; // adjust the pan value to align the centers of the graph and the canvas - meta.pan = rect.center().to_vec2() - graph_center * new_zoom; + let new_pan = rect.center().to_vec2() - graph_center * new_zoom; + self.set_pan(new_pan, meta); } fn handle_navigation( @@ -316,11 +322,16 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType> GraphView<'a, N, E, Ty> { return; } - if resp.dragged() && comp.dragged.is_none() { - meta.pan += resp.drag_delta(); + if resp.dragged() + && comp.dragged.is_none() + && (resp.drag_delta().x.abs() > 0. || resp.drag_delta().y.abs() > 0.) + { + let new_pan = meta.pan + resp.drag_delta(); + self.set_pan(new_pan, meta); } } + /// Zooms the graph by the given delta. It also compensates with pan to keep the zoom center in the same place. fn zoom(&self, rect: &Rect, delta: f32, zoom_center: Option, meta: &mut Metadata) { let center_pos = match zoom_center { Some(center_pos) => center_pos - rect.min, @@ -330,34 +341,39 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType> GraphView<'a, N, E, Ty> { let factor = 1. + delta; let new_zoom = meta.zoom * factor; - meta.pan += graph_center_pos * meta.zoom - graph_center_pos * new_zoom; - meta.zoom = new_zoom; + let pan_delta = graph_center_pos * meta.zoom - graph_center_pos * new_zoom; + let new_pan = meta.pan + pan_delta; + + self.set_pan(new_pan, meta); + self.set_zoom(new_zoom, meta); } - fn deselect_node(&mut self, idx: NodeIndex) { + fn select_node(&mut self, idx: NodeIndex) { let n = self.g.node_mut(idx).unwrap(); - let change = ChangeNode::change_selected(idx, n.selected(), false); - n.set_selected(false); + n.set_selected(true); - self.send_changes(Change::node(change)); + #[cfg(feature = "events")] + self.publish_event(Event::NodeSelect(PayloadNodeSelect { id: idx.index() })); } - fn select_node(&mut self, idx: NodeIndex) { + fn deselect_node(&mut self, idx: NodeIndex) { let n = self.g.node_mut(idx).unwrap(); - let change = ChangeNode::change_selected(idx, n.selected(), true); - n.set_selected(true); + n.set_selected(false); - self.send_changes(Change::node(change)); + #[cfg(feature = "events")] + self.publish_event(Event::NodeDeselect(PayloadNodeDeselect { id: idx.index() })); } fn set_node_clicked(&mut self, idx: NodeIndex) { - let change = ChangeNode::clicked(idx); - self.send_changes(Change::node(change)); + #[cfg(feature = "events")] + self.publish_event(Event::NodeClick(PayloadNodeClick { id: idx.index() })); } fn set_node_double_clicked(&mut self, idx: NodeIndex) { - let change = ChangeNode::double_clicked(idx); - self.send_changes(Change::node(change)); + #[cfg(feature = "events")] + self.publish_event(Event::NodeDoubleClick(PayloadNodeDoubleClick { + id: idx.index(), + })); } fn deselect_all(&mut self, comp: &ComputedState) { @@ -366,24 +382,55 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType> GraphView<'a, N, E, Ty> { }); } - fn set_dragged(&mut self, idx: NodeIndex, val: bool) { + fn move_node(&mut self, idx: NodeIndex, delta: Vec2) { let n = self.g.node_mut(idx).unwrap(); - let change = ChangeNode::change_dragged(idx, n.dragged(), val); - n.set_dragged(val); - self.send_changes(Change::node(change)); + n.set_location(n.location() + delta); + + #[cfg(feature = "events")] + self.publish_event(Event::NodeMove(PayloadNodeMove { + id: idx.index(), + diff: delta.into(), + })); } - fn move_node(&mut self, idx: NodeIndex, delta: Vec2) { + fn set_drag_start(&mut self, idx: NodeIndex) { let n = self.g.node_mut(idx).unwrap(); - let new_loc = n.location() + delta; - let change = ChangeNode::change_location(idx, n.location(), new_loc); - n.set_location(new_loc); - self.send_changes(Change::node(change)); + n.set_dragged(true); + + #[cfg(feature = "events")] + self.publish_event(Event::NodeDragStart(PayloadNodeDragStart { + id: idx.index(), + })); + } + + fn set_drag_end(&mut self, idx: NodeIndex) { + let n = self.g.node_mut(idx).unwrap(); + n.set_dragged(false); + + #[cfg(feature = "events")] + self.publish_event(Event::NodeDragEnd(PayloadNodeDragEnd { id: idx.index() })); + } + + fn set_pan(&self, val: Vec2, meta: &mut Metadata) { + let diff = val - meta.pan; + meta.pan = val; + + #[cfg(feature = "events")] + self.publish_event(Event::Pan(PayloadPan { diff: diff.into() })); + } + + fn set_zoom(&self, val: f32, meta: &mut Metadata) { + let diff = val - meta.zoom; + meta.zoom = val; + + #[cfg(feature = "events")] + self.publish_event(Event::Zoom(PyaloadZoom { diff })); } - fn send_changes(&self, changes: Change) { - if let Some(sender) = self.changes_sender { - sender.send(changes).unwrap(); + #[cfg(feature = "events")] + fn publish_event(&self, event: Event) { + if let Some(sender) = self.events_publisher { + sender.send(event).unwrap(); } } } diff --git a/src/lib.rs b/src/lib.rs index c32b44a..468a279 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,3 @@ -mod change; mod computed; mod elements; mod graph; @@ -9,7 +8,6 @@ mod transform; pub mod draw; -pub use self::change::{Change, ChangeEdge, ChangeNode}; pub use self::computed::ComputedNode; pub use self::elements::{Edge, Node}; pub use self::graph::Graph; @@ -20,3 +18,6 @@ pub use self::transform::{ add_edge, add_edge_custom, add_node, add_node_custom, default_edge_transform, default_node_transform, to_graph, to_graph_custom, }; + +#[cfg(feature = "events")] +pub mod events;