From 0e222fb6dd2b2a9ee5ede245a632231a6f3c729f Mon Sep 17 00:00:00 2001 From: starlord Date: Mon, 1 May 2023 19:26:31 +0400 Subject: [PATCH] Changed nodes API. Added interactice example (#27) - Dynamic generation selection - Node.radius is private field - selection depth param in SettingsStyle - primary selection color in SettingsStyle for directly selected node - secondary selection color in SettingsStyle for sub selections - added interactive example --- Cargo.lock | 20 +- Cargo.toml | 2 +- README.md | 2 +- examples/basic/Cargo.toml | 2 +- examples/configurable/Cargo.toml | 14 + examples/configurable/README.md | 8 + examples/configurable/src/main.rs | 649 ++++++++++++++++++ .../src/settings.rs | 0 examples/interactive/Cargo.toml | 4 +- examples/interactive/README.md | 7 +- examples/interactive/src/main.rs | 628 +---------------- src/changes.rs | 45 +- src/elements.rs | 111 ++- src/frame_state.rs | 86 +-- src/graph_view.rs | 222 +++--- src/lib.rs | 3 +- src/selections.rs | 168 +++++ src/settings.rs | 71 +- 18 files changed, 1234 insertions(+), 808 deletions(-) create mode 100644 examples/configurable/Cargo.toml create mode 100644 examples/configurable/README.md create mode 100644 examples/configurable/src/main.rs rename examples/{interactive => configurable}/src/settings.rs (100%) create mode 100644 src/selections.rs diff --git a/Cargo.lock b/Cargo.lock index 2eed56a..d3eff4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -339,7 +339,7 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "basic" -version = "0.2.0" +version = "0.3.0" dependencies = [ "eframe", "egui", @@ -519,6 +519,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "configurable" +version = "0.3.0" +dependencies = [ + "eframe", + "egui", + "egui_graphs", + "fdg-sim", + "petgraph", + "rand", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -739,7 +751,7 @@ dependencies = [ [[package]] name = "egui_graphs" -version = "0.2.0" +version = "0.3.0" dependencies = [ "egui", "petgraph", @@ -1133,14 +1145,12 @@ dependencies = [ [[package]] name = "interactive" -version = "0.2.0" +version = "0.3.0" dependencies = [ "eframe", "egui", "egui_graphs", - "fdg-sim", "petgraph", - "rand", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index de751ba..d0886c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "egui_graphs" -version = "0.2.0" +version = "0.3.0" authors = ["Dmitrii Samsonov "] license = "MIT" homepage = "https://github.com/blitzarx1/egui_graphs" diff --git a/README.md b/README.md index 98aefb4..6a109ab 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ You can further customize the appearance and behavior of your graph by modifying ### Interactive -You can check more advanced [interactive example](https://github.com/blitzarx1/egui_graph/tree/master/examples/interactive) for usage references and settings description. +You can check more advanced [configurable demo](https://github.com/blitzarx1/egui_graph/tree/master/examples/configurable) for usage references and settings description. ## Gallery ![ezgif-4-3e4e4469e6](https://user-images.githubusercontent.com/32969427/233863786-11459176-b741-4343-8b42-7d9b3a8239ee.gif) diff --git a/examples/basic/Cargo.toml b/examples/basic/Cargo.toml index dbd3fcc..ec5d97d 100644 --- a/examples/basic/Cargo.toml +++ b/examples/basic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "basic" -version = "0.2.0" +version = "0.3.0" authors = ["Dmitrii Samsonov "] license = "MIT" edition = "2021" diff --git a/examples/configurable/Cargo.toml b/examples/configurable/Cargo.toml new file mode 100644 index 0000000..affac7f --- /dev/null +++ b/examples/configurable/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "configurable" +version = "0.3.0" +authors = ["Dmitrii Samsonov "] +license = "MIT" +edition = "2021" + +[dependencies] +egui_graphs = { path = "../../" } +egui = "0.21.0" +eframe = "0.21.3" +petgraph = "0.6" +fdg-sim = "0.9.1" +rand = "0.8.4" diff --git a/examples/configurable/README.md b/examples/configurable/README.md new file mode 100644 index 0000000..a781f85 --- /dev/null +++ b/examples/configurable/README.md @@ -0,0 +1,8 @@ +# Configurable +Configurable example where you can toggle settings of the `GraphView` widget and see the result immediately. +It also contains controls to play with the graph. + +## run +```bash +cargo run --release -p configurable +``` diff --git a/examples/configurable/src/main.rs b/examples/configurable/src/main.rs new file mode 100644 index 0000000..afc0216 --- /dev/null +++ b/examples/configurable/src/main.rs @@ -0,0 +1,649 @@ +use std::sync::mpsc::{Receiver, Sender}; +use std::time::Instant; + +use eframe::{run_native, App, CreationContext}; +use egui::plot::{Line, Plot, PlotPoints}; +use egui::{CollapsingHeader, Color32, Context, Pos2, Rect, ScrollArea, Slider, Ui, Vec2, Visuals}; +use egui_graphs::{ + Changes, Edge, GraphView, Node, SettingsInteraction, SettingsNavigation, SettingsStyle, +}; +use fdg_sim::glam::Vec3; +use fdg_sim::{ForceGraph, ForceGraphHelper, Simulation, SimulationParameters}; +use petgraph::stable_graph::{EdgeIndex, NodeIndex, StableGraph}; +use petgraph::visit::EdgeRef; +use rand::Rng; +use settings::SettingsGraph; + +mod settings; + +const SIMULATION_DT: f32 = 0.035; +const INITIAL_RECT_SIZE: f32 = 200.; +const FPS_LINE_COLOR: Color32 = Color32::from_rgb(128, 128, 128); +const CHANGES_LIMIT: usize = 100; + +pub struct ConfigurableApp { + g: StableGraph, Edge<()>>, + sim: Simulation<(), ()>, + + settings_graph: SettingsGraph, + settings_interaction: SettingsInteraction, + settings_navigation: SettingsNavigation, + settings_style: SettingsStyle, + + selected_nodes: Vec>, + selected_edges: Vec>, + last_changes: Vec, + + simulation_stopped: bool, + dark_mode: bool, + + fps: f64, + fps_history: Vec, + last_update_time: Instant, + frames_last_time_span: usize, + + changes_receiver: Receiver, + changes_sender: Sender, +} + +impl ConfigurableApp { + fn new(_: &CreationContext<'_>) -> Self { + let settings_graph = SettingsGraph::default(); + let (g, sim) = generate(&settings_graph); + let (changes_sender, changes_receiver) = std::sync::mpsc::channel(); + Self { + g, + sim, + + changes_receiver, + changes_sender, + + settings_graph, + + settings_interaction: Default::default(), + settings_navigation: Default::default(), + settings_style: Default::default(), + + selected_nodes: Default::default(), + selected_edges: Default::default(), + last_changes: Default::default(), + + simulation_stopped: false, + dark_mode: true, + + fps: 0., + fps_history: Default::default(), + last_update_time: Instant::now(), + frames_last_time_span: 0, + } + } + + fn update_simulation(&mut self) { + if self.simulation_stopped { + return; + } + + // the following manipulations is a hack to avoid having looped edges in the simulation + // because they cause the simulation to blow up; this is the issue of the fdg_sim engine + // we use for the simulation + // * remove loop edges + // * update simulation + // * restore loop edges + + // remove looped edges + let looped_nodes = { + let graph = self.sim.get_graph_mut(); + let mut looped_nodes = vec![]; + let mut looped_edges = vec![]; + graph.edge_indices().for_each(|idx| { + let edge = graph.edge_endpoints(idx).unwrap(); + let looped = edge.0 == edge.1; + if looped { + looped_nodes.push((edge.0, ())); + looped_edges.push(idx); + } + }); + + for idx in looped_edges { + graph.remove_edge(idx); + } + + self.sim.update(SIMULATION_DT); + + looped_nodes + }; + + // restore looped edges + let graph = self.sim.get_graph_mut(); + for (idx, _) in looped_nodes.iter() { + graph.add_edge(*idx, *idx, ()); + } + } + + /// Syncs the graph with the simulation. + /// + /// Changes location of nodes in `g` according to the locations in `sim`. If node from `g` is dragged its location is prioritized + /// over the location of the corresponding node from `sim` and this location is set to the node from the `sim`. + /// + /// If node or edge is selected it is added to the corresponding selected field in `self`. + fn sync_graph_with_simulation(&mut self) { + self.selected_nodes = vec![]; + self.selected_edges = vec![]; + + let g_indices = self.g.node_indices().collect::>(); + g_indices.iter().for_each(|g_n_idx| { + let g_n = self.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.location = Vec2::new(loc.x, loc.y); + + if g_n.selected { + self.selected_nodes.push(*g_n); + } + }); + + self.g.edge_weights().for_each(|g_e| { + if g_e.selected { + self.selected_edges.push(*g_e); + } + }); + } + + fn update_fps(&mut self) { + self.frames_last_time_span += 1; + let now = Instant::now(); + let elapsed = now.duration_since(self.last_update_time); + if elapsed.as_secs() >= 1 { + 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); + } + } + } + + fn reset_graph(&mut self, ui: &mut Ui) { + let settings_graph = SettingsGraph::default(); + let (g, sim) = generate(&settings_graph); + + self.g = g; + self.sim = sim; + self.settings_graph = settings_graph; + self.last_changes = Default::default(); + + GraphView::<(), ()>::reset_metadata(ui); + } + + fn handle_changes(&mut self) { + self.changes_receiver.try_iter().for_each(|changes| { + if self.last_changes.len() > CHANGES_LIMIT { + self.last_changes.remove(0); + } + + self.last_changes.push(changes); + }); + } + + fn random_node_idx(&self) -> Option { + let nodes_cnt = self.g.node_count(); + if nodes_cnt == 0 { + return None; + } + + let mut rng = rand::thread_rng(); + let random_n_idx = rng.gen_range(0..nodes_cnt); + self.g.node_indices().nth(random_n_idx) + } + + fn random_edge_idx(&self) -> Option { + let edges_cnt = self.g.edge_count(); + if edges_cnt == 0 { + return None; + } + + let mut rng = rand::thread_rng(); + let random_e_idx = rng.gen_range(0..edges_cnt); + self.g.edge_indices().nth(random_e_idx) + } + + fn remove_random_node(&mut self) { + let idx = self.random_node_idx().unwrap(); + self.remove_node(idx); + } + + fn add_random_node(&mut self) { + let random_n_idx = self.random_node_idx(); + if random_n_idx.is_none() { + return; + } + + let random_n = self.g.node_weight(random_n_idx.unwrap()).unwrap(); + + // location of new node is in surrounging of random existing node + let mut rng = rand::thread_rng(); + let location = Vec2::new( + random_n.location.x + random_n.radius() + rng.gen_range(0. ..(random_n.radius() * 5.)), + random_n.location.y + random_n.radius() + rng.gen_range(0. ..(random_n.radius() * 5.)), + ); + + let idx = self.g.add_node(Node::new(location, ())); + let mut sim_node = fdg_sim::Node::new(format!("{}", idx.index()).as_str(), ()); + sim_node.location = Vec3::new(location.x, location.y, 0.); + self.sim.get_graph_mut().add_node(sim_node); + } + + fn remove_node(&mut self, idx: NodeIndex) { + // before removing nodes we need to remove all edges connected to it + let neighbors = self.g.neighbors_undirected(idx).collect::>(); + neighbors.iter().for_each(|n| { + self.remove_edges(idx, *n); + self.remove_edges(*n, idx); + }); + + self.g.remove_node(idx).unwrap(); + self.sim.get_graph_mut().remove_node(idx).unwrap(); + + // update edges count + self.settings_graph.count_edge = self.g.edge_count(); + } + + fn add_random_edge(&mut self) { + let random_start = self.random_node_idx().unwrap(); + let random_end = self.random_node_idx().unwrap(); + + self.add_edge(random_start, random_end); + } + + fn add_edge(&mut self, start: NodeIndex, end: NodeIndex) { + self.g.add_edge(start, end, Edge::new(())); + self.sim.get_graph_mut().add_edge(start, end, ()); + } + + fn remove_random_edge(&mut self) { + let random_e_idx = self.random_edge_idx(); + if random_e_idx.is_none() { + return; + } + let endpoints = self.g.edge_endpoints(random_e_idx.unwrap()).unwrap(); + + self.remove_edge(endpoints.0, endpoints.1); + } + + /// Removes random edge. Can not remove edge by idx because + /// there can be multiple edges between two nodes in 2 graphs + /// and we can't be sure that they are indexed the same way. + fn remove_edge(&mut self, start: NodeIndex, end: NodeIndex) { + let g_idx = self.g.find_edge(start, end); + if g_idx.is_none() { + return; + } + + self.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(); + } + + /// Removes all edges between two nodes + fn remove_edges(&mut self, start: NodeIndex, end: NodeIndex) { + let g_idxs = self + .g + .edges_connecting(start, end) + .map(|e| e.id()) + .collect::>(); + if g_idxs.is_empty() { + return; + } + + g_idxs.iter().for_each(|e| { + self.g.remove_edge(*e).unwrap(); + }); + + let sim_idxs = self + .sim + .get_graph() + .edges_connecting(start, end) + .map(|e| e.id()) + .collect::>(); + + sim_idxs.iter().for_each(|e| { + self.sim.get_graph_mut().remove_edge(*e).unwrap(); + }); + } + + fn draw_section_client(&mut self, ui: &mut Ui) { + CollapsingHeader::new("Client") + .default_open(true) + .show(ui, |ui| { + ui.add_space(10.); + + ui.label("Simulation"); + ui.separator(); + + ui.horizontal(|ui| { + if ui + .button(match self.simulation_stopped { + true => "start", + false => "stop", + }) + .clicked() + { + self.simulation_stopped = !self.simulation_stopped; + }; + if ui.button("reset").clicked() { + self.reset_graph(ui); + } + }); + + ui.add_space(10.); + + self.draw_counts_sliders(ui); + + ui.add_space(10.); + + ui.label("Style"); + ui.separator(); + + self.draw_dark_mode(ui); + }); + } + + fn draw_section_widget(&mut self, ui: &mut Ui) { + 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, "fit_to_screen") + .changed() + && self.settings_navigation.fit_to_screen + { + self.settings_navigation.zoom_and_pan = 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, |ui| { + ui.vertical(|ui| { + ui.checkbox(&mut self.settings_navigation.zoom_and_pan, "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.); + + ui.label("SettingsStyle"); + ui.separator(); + + 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(10.); + + ui.label("SettingsInteraction"); + ui.separator(); + + ui.checkbox(&mut self.settings_interaction.node_drag, "node_drag"); + ui.label("To drag use LMB + drag on a node."); + + ui.add_space(5.); + + ui.add_enabled_ui(!self.settings_interaction.node_multiselect, |ui| { + ui.vertical(|ui| { + ui.checkbox(&mut self.settings_interaction.node_select, "node_select"); + 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("node_multiselect enables select"); + }); + + ui.add_space(5.); + + ui.add(Slider::new(&mut self.settings_interaction.selection_depth, -10..=10) + .text("selection_depth")); + ui.label("How deep into the neighbours of selected nodes should the selection go."); + + ui.add_space(5.); + + if ui.checkbox(&mut self.settings_interaction.node_multiselect, "node_multiselect").changed() { + self.settings_interaction.node_select = true; + } + ui.label("Enable multiselect to select multiple nodes."); + + ui.add_space(5.); + + ui.collapsing("selected", |ui| { + ScrollArea::vertical().max_height(200.).show(ui, |ui| { + self.selected_nodes.iter().for_each(|node| { + ui.label(format!("{:?}", node)); + }); + self.selected_edges.iter().for_each(|edge| { + ui.label(format!("{:?}", edge)); + }); + }); + }); + + 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)); + }); + }); + }); + }); + } + + fn draw_section_debug(&mut self, ui: &mut Ui) { + CollapsingHeader::new("Debug") + .default_open(false) + .show(ui, |ui| { + ui.add_space(10.); + + ui.vertical(|ui| { + ui.label(format!("fps: {:.1}", self.fps)); + ui.add_space(10.); + self.draw_fps(ui); + }); + }); + } + + fn draw_dark_mode(&mut self, ui: &mut Ui) { + if self.dark_mode { + ui.ctx().set_visuals(Visuals::dark()) + } else { + ui.ctx().set_visuals(Visuals::light()) + } + + if ui + .button({ + match self.dark_mode { + true => "🔆 light", + false => "🌙 dark", + } + }) + .clicked() + { + self.dark_mode = !self.dark_mode + }; + } + + 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; + + ui.add(Slider::new(&mut self.settings_graph.count_node, 1..=2500).text("nodes")); + + let delta = self.settings_graph.count_node as i32 - before; + (0..delta.abs()).for_each(|_| { + if delta > 0 { + self.add_random_node(); + return; + }; + self.remove_random_node(); + }); + }); + + ui.horizontal(|ui| { + let before = self.settings_graph.count_edge as i32; + + ui.add(Slider::new(&mut self.settings_graph.count_edge, 0..=5000).text("edges")); + + let delta = self.settings_graph.count_edge as i32 - before; + (0..delta.abs()).for_each(|_| { + if delta > 0 { + self.add_random_edge(); + return; + }; + self.remove_random_edge(); + }); + }); + } +} + +impl App for ConfigurableApp { + fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { + egui::SidePanel::right("right_panel") + .min_width(250.) + .show(ctx, |ui| { + ScrollArea::vertical().show(ui, |ui| { + self.draw_section_client(ui); + + ui.add_space(10.); + + self.draw_section_widget(ui); + + ui.add_space(10.); + + self.draw_section_debug(ui); + }); + }); + + egui::CentralPanel::default().show(ctx, |ui| { + ui.add( + &mut GraphView::new(&mut self.g) + .with_interactions(&self.settings_interaction) + .with_navigations(&self.settings_navigation) + .with_styles(&self.settings_style) + .with_changes(&self.changes_sender), + ); + }); + + self.handle_changes(); + self.sync_graph_with_simulation(); + + self.update_simulation(); + self.update_fps(); + } +} + +fn generate(settings: &SettingsGraph) -> (StableGraph, Edge<()>>, Simulation<(), ()>) { + let g = generate_random_graph(settings.count_node, settings.count_edge); + let sim = construct_simulation(&g); + + (g, sim) +} + +fn construct_simulation(g: &StableGraph, Edge<()>>) -> Simulation<(), ()> { + // create force graph + let mut force_graph = ForceGraph::with_capacity(g.node_count(), g.edge_count()); + g.node_indices().for_each(|idx| { + let idx = idx.index(); + force_graph.add_force_node(format!("{}", idx).as_str(), ()); + }); + g.edge_indices().for_each(|idx| { + let (source, target) = g.edge_endpoints(idx).unwrap(); + force_graph.add_edge(source, target, ()); + }); + + // initialize simulation + let mut params = SimulationParameters::default(); + let force = fdg_sim::force::fruchterman_reingold(100., 0.5); + params.set_force(force); + + Simulation::from_graph(force_graph, params) +} + +fn generate_random_graph(node_count: usize, edge_count: usize) -> StableGraph, Edge<()>> { + let mut rng = rand::thread_rng(); + let mut graph = StableGraph::new(); + let rect = &Rect::from_min_max( + Pos2::new(-INITIAL_RECT_SIZE, -INITIAL_RECT_SIZE), + Pos2::new(INITIAL_RECT_SIZE, INITIAL_RECT_SIZE), + ); + + // add nodes + for _ in 0..node_count { + graph.add_node(Node::new(random_point(rect), ())); + } + + // add random edges + for _ in 0..edge_count { + let source = rng.gen_range(0..node_count); + let target = rng.gen_range(0..node_count); + + graph.add_edge( + NodeIndex::new(source), + NodeIndex::new(target), + Edge::new(()), + ); + } + + graph +} + +fn random_point(rect: &Rect) -> Vec2 { + let mut rng = rand::thread_rng(); + + let x = rng.gen_range(rect.left()..rect.right()); + let y = rng.gen_range(rect.top()..rect.bottom()); + + Vec2::new(x, y) +} + +fn main() { + let native_options = eframe::NativeOptions::default(); + run_native( + "egui_graphs_configurable_demo", + native_options, + Box::new(|cc| Box::new(ConfigurableApp::new(cc))), + ) + .unwrap(); +} diff --git a/examples/interactive/src/settings.rs b/examples/configurable/src/settings.rs similarity index 100% rename from examples/interactive/src/settings.rs rename to examples/configurable/src/settings.rs diff --git a/examples/interactive/Cargo.toml b/examples/interactive/Cargo.toml index 3340dfa..ef261f9 100644 --- a/examples/interactive/Cargo.toml +++ b/examples/interactive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "interactive" -version = "0.2.0" +version = "0.3.0" authors = ["Dmitrii Samsonov "] license = "MIT" edition = "2021" @@ -10,5 +10,3 @@ egui_graphs = { path = "../../" } egui = "0.21.0" eframe = "0.21.3" petgraph = "0.6" -fdg-sim = "0.9.1" -rand = "0.8.4" diff --git a/examples/interactive/README.md b/examples/interactive/README.md index b0d39bc..35fd5e7 100644 --- a/examples/interactive/README.md +++ b/examples/interactive/README.md @@ -1,8 +1,9 @@ # Interactive -Interactive example where you can toggle settings of the `GraphView` widget and see the result immediately. -It also contains controls to play with the graph. +Modification of the [basic example](https://github.com/blitzarx1/egui_graph/tree/master/examples/basic) which shows how easy it is to enable interactivity. + +Try to drag around and select some nodes. ## run ```bash cargo run --release -p interactive -``` +``` \ No newline at end of file diff --git a/examples/interactive/src/main.rs b/examples/interactive/src/main.rs index fd9a3ae..ea9931a 100644 --- a/examples/interactive/src/main.rs +++ b/examples/interactive/src/main.rs @@ -1,633 +1,49 @@ -use std::sync::mpsc::{Receiver, Sender}; -use std::time::Instant; - use eframe::{run_native, App, CreationContext}; -use egui::plot::{Line, Plot, PlotPoints}; -use egui::{CollapsingHeader, Color32, Context, Pos2, Rect, ScrollArea, Slider, Ui, Vec2, Visuals}; -use egui_graphs::{ - Changes, Edge, GraphView, Node, SettingsInteraction, SettingsNavigation, SettingsStyle, -}; -use fdg_sim::glam::Vec3; -use fdg_sim::{ForceGraph, ForceGraphHelper, Simulation, SimulationParameters}; -use petgraph::stable_graph::{EdgeIndex, NodeIndex, StableGraph}; -use petgraph::visit::EdgeRef; -use rand::Rng; -use settings::SettingsGraph; - -mod settings; +use egui::Context; +use egui_graphs::{Edge, GraphView, Node, SettingsInteraction}; +use petgraph::stable_graph::StableGraph; -const SIMULATION_DT: f32 = 0.035; -const INITIAL_RECT_SIZE: f32 = 200.; -const FPS_LINE_COLOR: Color32 = Color32::from_rgb(128, 128, 128); -const CHANGES_LIMIT: usize = 100; +const SIDE_SIZE: f32 = 50.; pub struct InteractiveApp { g: StableGraph, Edge<()>>, - sim: Simulation<(), ()>, - - settings_graph: SettingsGraph, - settings_interaction: SettingsInteraction, - settings_navigation: SettingsNavigation, - settings_style: SettingsStyle, - - selected_nodes: Vec>, - selected_edges: Vec>, - last_changes: Vec, - - simulation_stopped: bool, - dark_mode: bool, - - fps: f64, - fps_history: Vec, - last_update_time: Instant, - frames_last_time_span: usize, - - changes_receiver: Receiver, - changes_sender: Sender, } impl InteractiveApp { fn new(_: &CreationContext<'_>) -> Self { - let settings_graph = SettingsGraph::default(); - let (g, sim) = generate(&settings_graph); - let (changes_sender, changes_receiver) = std::sync::mpsc::channel(); - Self { - g, - sim, - - changes_receiver, - changes_sender, - - settings_graph, - - settings_interaction: Default::default(), - settings_navigation: Default::default(), - settings_style: Default::default(), - - selected_nodes: Default::default(), - selected_edges: Default::default(), - last_changes: Default::default(), - - simulation_stopped: false, - dark_mode: true, - - fps: 0., - fps_history: Default::default(), - last_update_time: Instant::now(), - frames_last_time_span: 0, - } - } - - fn update_simulation(&mut self) { - if self.simulation_stopped { - return; - } - - // the following manipulations is a hack to avoid having looped edges in the simulation - // because they cause the simulation to blow up; this is the issue of the fdg_sim engine - // we use for the simulation - // * remove loop edges - // * update simulation - // * restore loop edges - - // remove looped edges - let looped_nodes = { - let graph = self.sim.get_graph_mut(); - let mut looped_nodes = vec![]; - let mut looped_edges = vec![]; - graph.edge_indices().for_each(|idx| { - let edge = graph.edge_endpoints(idx).unwrap(); - let looped = edge.0 == edge.1; - if looped { - looped_nodes.push((edge.0, ())); - looped_edges.push(idx); - } - }); - - for idx in looped_edges { - graph.remove_edge(idx); - } - - self.sim.update(SIMULATION_DT); - - looped_nodes - }; - - // restore looped edges - let graph = self.sim.get_graph_mut(); - for (idx, _) in looped_nodes.iter() { - graph.add_edge(*idx, *idx, ()); - } - } - - /// Syncs the graph with the simulation. - /// - /// Changes location of nodes in `g` according to the locations in `sim`. If node from `g` is dragged its location is prioritized - /// over the location of the corresponding node from `sim` and this location is set to the node from the `sim`. - /// - /// If node or edge is selected it is added to the corresponding selected field in `self`. - fn sync_graph_with_simulation(&mut self) { - self.selected_nodes = vec![]; - self.selected_edges = vec![]; - - let g_indices = self.g.node_indices().collect::>(); - g_indices.iter().for_each(|g_n_idx| { - let g_n = self.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.location = Vec2::new(loc.x, loc.y); - - if g_n.selected { - self.selected_nodes.push(*g_n); - } - }); - - self.g.edge_weights().for_each(|g_e| { - if g_e.selected { - self.selected_edges.push(*g_e); - } - }); - } - - fn update_fps(&mut self) { - self.frames_last_time_span += 1; - let now = Instant::now(); - let elapsed = now.duration_since(self.last_update_time); - if elapsed.as_secs() >= 1 { - 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); - } - } - } - - fn reset_graph(&mut self, ui: &mut Ui) { - let settings_graph = SettingsGraph::default(); - let (g, sim) = generate(&settings_graph); - - self.g = g; - self.sim = sim; - self.settings_graph = settings_graph; - - GraphView::<(), ()>::reset_metadata(ui); - } - - fn handle_changes(&mut self) { - self.changes_receiver.try_iter().for_each(|changes| { - if self.last_changes.len() > CHANGES_LIMIT { - self.last_changes[0] = changes; - return; - } - - self.last_changes.push(changes); - }); - } - - fn random_node_idx(&self) -> Option { - let nodes_cnt = self.g.node_count(); - if nodes_cnt == 0 { - return None; - } - - let mut rng = rand::thread_rng(); - let random_n_idx = rng.gen_range(0..nodes_cnt); - self.g.node_indices().nth(random_n_idx) - } - - fn random_edge_idx(&self) -> Option { - let edges_cnt = self.g.edge_count(); - if edges_cnt == 0 { - return None; - } - - let mut rng = rand::thread_rng(); - let random_e_idx = rng.gen_range(0..edges_cnt); - self.g.edge_indices().nth(random_e_idx) - } - - fn remove_random_node(&mut self) { - let idx = self.random_node_idx().unwrap(); - self.remove_node(idx); - } - - fn add_random_node(&mut self) { - let random_n_idx = self.random_node_idx(); - if random_n_idx.is_none() { - return; - } - - let random_n = self.g.node_weight(random_n_idx.unwrap()).unwrap(); - - // location of new node is in surrounging of random existing node - let mut rng = rand::thread_rng(); - let location = Vec2::new( - random_n.location.x + random_n.radius + rng.gen_range(0. ..(random_n.radius * 5.)), - random_n.location.y + random_n.radius + rng.gen_range(0. ..(random_n.radius * 5.)), - ); - - let idx = self.g.add_node(Node::new(location, ())); - let mut sim_node = fdg_sim::Node::new(format!("{}", idx.index()).as_str(), ()); - sim_node.location = Vec3::new(location.x, location.y, 0.); - self.sim.get_graph_mut().add_node(sim_node); - } - - fn remove_node(&mut self, idx: NodeIndex) { - // before removing nodes we need to remove all edges connected to it - let neighbors = self.g.neighbors_undirected(idx).collect::>(); - neighbors.iter().for_each(|n| { - self.remove_edges(idx, *n); - self.remove_edges(*n, idx); - }); - - self.g.remove_node(idx).unwrap(); - self.sim.get_graph_mut().remove_node(idx).unwrap(); - - // update edges count - self.settings_graph.count_edge = self.g.edge_count(); - } - - fn add_random_edge(&mut self) { - let random_start = self.random_node_idx().unwrap(); - let random_end = self.random_node_idx().unwrap(); - - self.add_edge(random_start, random_end); - } - - fn add_edge(&mut self, start: NodeIndex, end: NodeIndex) { - self.g.add_edge(start, end, Edge::new(())); - self.sim.get_graph_mut().add_edge(start, end, ()); - } - - fn remove_random_edge(&mut self) { - let random_e_idx = self.random_edge_idx(); - if random_e_idx.is_none() { - return; - } - let endpoints = self.g.edge_endpoints(random_e_idx.unwrap()).unwrap(); - - self.remove_edge(endpoints.0, endpoints.1); - } - - /// Removes random edge. Can not remove edge by idx because - /// there can be multiple edges between two nodes in 2 graphs - /// and we can't be sure that they are indexed the same way. - fn remove_edge(&mut self, start: NodeIndex, end: NodeIndex) { - let g_idx = self.g.find_edge(start, end); - if g_idx.is_none() { - return; - } - - self.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(); - } - - /// Removes all edges between two nodes - fn remove_edges(&mut self, start: NodeIndex, end: NodeIndex) { - let g_idxs = self - .g - .edges_connecting(start, end) - .map(|e| e.id()) - .collect::>(); - if g_idxs.is_empty() { - return; - } - - g_idxs.iter().for_each(|e| { - self.g.remove_edge(*e).unwrap(); - }); - - let sim_idxs = self - .sim - .get_graph() - .edges_connecting(start, end) - .map(|e| e.id()) - .collect::>(); - - sim_idxs.iter().for_each(|e| { - self.sim.get_graph_mut().remove_edge(*e).unwrap(); - }); - } - - fn draw_section_client(&mut self, ui: &mut Ui) { - CollapsingHeader::new("Client") - .default_open(true) - .show(ui, |ui| { - ui.add_space(10.); - - ui.label("Simulation"); - ui.separator(); - - ui.horizontal(|ui| { - if ui - .button(match self.simulation_stopped { - true => "start", - false => "stop", - }) - .clicked() - { - self.simulation_stopped = !self.simulation_stopped; - }; - if ui.button("reset").clicked() { - self.reset_graph(ui); - } - }); - - ui.add_space(10.); - - self.draw_counts_sliders(ui); - - ui.add_space(10.); - - ui.label("Style"); - ui.separator(); - - self.draw_dark_mode(ui); - }); - } - - fn draw_section_widget(&mut self, ui: &mut Ui) { - 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, "fit_to_screen") - .changed() - && self.settings_navigation.fit_to_screen - { - self.settings_navigation.zoom_and_pan = 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, |ui| { - ui.vertical(|ui| { - ui.checkbox(&mut self.settings_navigation.zoom_and_pan, "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.); - - ui.label("SettingsStyle"); - ui.separator(); - - ui.add(Slider::new(&mut self.settings_style.edge_radius_weight, 1.0..=5.0) - .text("edge_radius_weight")); - ui.label("For every edge connected to node its radius is getting bigger by this value."); - - ui.add_space(10.); - - ui.label("SettingsInteraction"); - ui.separator(); - - ui.checkbox(&mut self.settings_interaction.node_drag, "node_drag"); - ui.label("To drag use LMB + drag on a node."); - - ui.add_space(5.); - - ui.add_enabled_ui(!self.settings_interaction.node_multiselect, |ui| { - ui.vertical(|ui| { - ui.checkbox(&mut self.settings_interaction.node_select, "node_select"); - 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("node_multiselect enables select"); - }); - - ui.add_space(5.); - - if ui.checkbox(&mut self.settings_interaction.node_multiselect, "node_multiselect").changed() { - self.settings_interaction.node_select = true; - } - ui.label("Enable multiselect to select multiple nodes."); - - ui.add_space(5.); - - ui.collapsing("selected", |ui| { - ScrollArea::vertical().max_height(200.).show(ui, |ui| { - self.selected_nodes.iter().for_each(|node| { - ui.label(format!("{:?}", node)); - }); - self.selected_edges.iter().for_each(|edge| { - ui.label(format!("{:?}", edge)); - }); - }); - }); - - ui.collapsing("last changes", |ui| { - ScrollArea::vertical().max_height(200.).show(ui, |ui| { - self.last_changes.iter().for_each(|node| { - ui.label(format!("{:?}", node)); - }); - }); - }); - }); - } - - fn draw_section_debug(&mut self, ui: &mut Ui) { - CollapsingHeader::new("Debug") - .default_open(false) - .show(ui, |ui| { - ui.add_space(10.); - - ui.vertical(|ui| { - ui.label(format!("fps: {:.1}", self.fps)); - ui.add_space(10.); - self.draw_fps(ui); - }); - }); - } - - fn draw_dark_mode(&mut self, ui: &mut Ui) { - if self.dark_mode { - ui.ctx().set_visuals(Visuals::dark()) - } else { - ui.ctx().set_visuals(Visuals::light()) - } - - if ui - .button({ - match self.dark_mode { - true => "🔆 light", - false => "🌙 dark", - } - }) - .clicked() - { - self.dark_mode = !self.dark_mode - }; - } - - 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; - - ui.add(Slider::new(&mut self.settings_graph.count_node, 1..=2500).text("nodes")); - - let delta = self.settings_graph.count_node as i32 - before; - (0..delta.abs()).for_each(|_| { - if delta > 0 { - self.add_random_node(); - return; - }; - self.remove_random_node(); - }); - }); - - ui.horizontal(|ui| { - let before = self.settings_graph.count_edge as i32; - - ui.add(Slider::new(&mut self.settings_graph.count_edge, 0..=5000).text("edges")); - - let delta = self.settings_graph.count_edge as i32 - before; - (0..delta.abs()).for_each(|_| { - if delta > 0 { - self.add_random_edge(); - return; - }; - self.remove_random_edge(); - }); - }); + let g = generate_graph(); + Self { g } } } impl App for InteractiveApp { fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { - egui::SidePanel::right("right_panel") - .min_width(250.) - .show(ctx, |ui| { - ScrollArea::vertical().show(ui, |ui| { - self.draw_section_client(ui); - - ui.add_space(10.); - - self.draw_section_widget(ui); - - ui.add_space(10.); - - self.draw_section_debug(ui); - }); - }); - egui::CentralPanel::default().show(ctx, |ui| { ui.add( - &mut GraphView::new(&mut self.g) - .with_interactions(&self.settings_interaction, &self.changes_sender) - .with_navigations(&self.settings_navigation) - .with_styles(&self.settings_style), + &mut GraphView::new(&mut self.g).with_interactions(&SettingsInteraction { + node_drag: true, + node_click: true, + node_select: true, + node_multiselect: true, + ..Default::default() + }), ); }); - - self.handle_changes(); - self.sync_graph_with_simulation(); - - self.update_simulation(); - self.update_fps(); } } -fn generate(settings: &SettingsGraph) -> (StableGraph, Edge<()>>, Simulation<(), ()>) { - let g = generate_random_graph(settings.count_node, settings.count_edge); - let sim = construct_simulation(&g); - - (g, sim) -} - -fn construct_simulation(g: &StableGraph, Edge<()>>) -> Simulation<(), ()> { - // create force graph - let mut force_graph = ForceGraph::with_capacity(g.node_count(), g.edge_count()); - g.node_indices().for_each(|idx| { - let idx = idx.index(); - force_graph.add_force_node(format!("{}", idx).as_str(), ()); - }); - g.edge_indices().for_each(|idx| { - let (source, target) = g.edge_endpoints(idx).unwrap(); - force_graph.add_edge(source, target, ()); - }); - - // initialize simulation - let mut params = SimulationParameters::default(); - let force = fdg_sim::force::fruchterman_reingold(100., 0.5); - params.set_force(force); - - Simulation::from_graph(force_graph, params) -} - -fn generate_random_graph(node_count: usize, edge_count: usize) -> StableGraph, Edge<()>> { - let mut rng = rand::thread_rng(); - let mut graph = StableGraph::new(); - let rect = &Rect::from_min_max( - Pos2::new(-INITIAL_RECT_SIZE, -INITIAL_RECT_SIZE), - Pos2::new(INITIAL_RECT_SIZE, INITIAL_RECT_SIZE), - ); - - // add nodes - for _ in 0..node_count { - graph.add_node(Node::new(random_point(rect), ())); - } - - // add random edges - for _ in 0..edge_count { - let source = rng.gen_range(0..node_count); - let target = rng.gen_range(0..node_count); - - graph.add_edge( - NodeIndex::new(source), - NodeIndex::new(target), - Edge::new(()), - ); - } - - graph -} +fn generate_graph() -> StableGraph, Edge<()>> { + let mut g: StableGraph, Edge<()>> = StableGraph::new(); -fn random_point(rect: &Rect) -> Vec2 { - let mut rng = rand::thread_rng(); + let a = g.add_node(Node::new(egui::Vec2::new(0., SIDE_SIZE), ())); + let b = g.add_node(Node::new(egui::Vec2::new(-SIDE_SIZE, 0.), ())); + let c = g.add_node(Node::new(egui::Vec2::new(SIDE_SIZE, 0.), ())); - let x = rng.gen_range(rect.left()..rect.right()); - let y = rng.gen_range(rect.top()..rect.bottom()); + g.add_edge(a, b, Edge::new(())); + g.add_edge(b, c, Edge::new(())); + g.add_edge(c, a, Edge::new(())); - Vec2::new(x, y) + g } fn main() { diff --git a/src/changes.rs b/src/changes.rs index cef9d1d..ca073b8 100644 --- a/src/changes.rs +++ b/src/changes.rs @@ -6,10 +6,14 @@ use petgraph::stable_graph::NodeIndex; /// `Changes` is a struct that stores the changes that happened in the graph #[derive(Default, Debug, Clone)] pub struct Changes { - pub(crate) nodes: HashMap, + pub nodes: HashMap, } impl Changes { + pub fn is_empty(&self) -> bool { + self.nodes.is_empty() + } + pub(crate) fn set_location(&mut self, idx: NodeIndex, val: Vec2) { match self.nodes.get_mut(&idx) { Some(changes_node) => changes_node.set_location(val), @@ -32,12 +36,23 @@ impl Changes { }; } - pub(crate) fn set_selected(&mut self, idx: NodeIndex, val: bool) { + pub(crate) fn select_node(&mut self, idx: NodeIndex) { match self.nodes.get_mut(&idx) { - Some(changes_node) => changes_node.set_selected(val), + Some(changes_node) => changes_node.select(), None => { let mut changes_node = ChangesNode::default(); - changes_node.set_selected(val); + changes_node.select(); + self.nodes.insert(idx, changes_node); + } + }; + } + + pub(crate) fn deselect_node(&mut self, idx: NodeIndex) { + match self.nodes.get_mut(&idx) { + Some(changes_node) => changes_node.deselect(), + None => { + let mut changes_node = ChangesNode::default(); + changes_node.deselect(); self.nodes.insert(idx, changes_node); } }; @@ -66,19 +81,23 @@ pub struct ChangesNode { impl ChangesNode { fn set_location(&mut self, new_location: Vec2) { - self.location.get_or_insert(new_location); + self.location = Some(new_location); } - fn set_selected(&mut self, new_selected: bool) { - self.selected.get_or_insert(new_selected); + fn select(&mut self) { + self.selected = Some(true) + } + + fn deselect(&mut self) { + self.selected = Some(false); } fn set_dragged(&mut self, new_dragged: bool) { - self.dragged.get_or_insert(new_dragged); + self.dragged = Some(new_dragged); } fn set_clicked(&mut self, new_clicked: bool) { - self.clicked.get_or_insert(new_clicked); + self.clicked = Some(new_clicked); } } @@ -114,9 +133,11 @@ mod tests { changes.set_clicked(idx, clicked); assert_eq!(changes.nodes.get(&idx).unwrap().clicked.unwrap(), clicked); - let selected = true; - changes.set_selected(idx, selected); - assert_eq!(changes.nodes.get(&idx).unwrap().selected.unwrap(), selected); + changes.select_node(idx); + assert!(changes.nodes.get(&idx).unwrap().selected.unwrap()); + + changes.deselect_node(idx); + assert!(!changes.nodes.get(&idx).unwrap().selected.unwrap()); let dragged = true; changes.set_dragged(idx, dragged); diff --git a/src/elements.rs b/src/elements.rs index e8e6697..90a01a8 100644 --- a/src/elements.rs +++ b/src/elements.rs @@ -4,29 +4,47 @@ use egui::{Color32, Vec2}; #[derive(Clone, Debug, Copy, PartialEq)] pub struct Node { /// Client data - pub data: N, + pub data: Option, pub location: Vec2, /// If `color` is None default color is used. pub color: Option, - pub radius: f32, pub selected: bool, pub dragged: bool, + + /// This field is recomputed on every frame + pub(crate) radius: f32, + /// This field is recomputed on every frame + pub(crate) selected_child: bool, + /// This field is recomputed on every frame + pub(crate) selected_parent: bool, +} + +impl Default for Node { + fn default() -> Self { + Self { + radius: 5., + + location: Default::default(), + data: Default::default(), + color: Default::default(), + selected: Default::default(), + dragged: Default::default(), + selected_child: Default::default(), + selected_parent: Default::default(), + } + } } impl Node { pub fn new(location: Vec2, data: N) -> Self { Self { location, - data, + data: Some(data), - color: None, - radius: 5., - - selected: false, - dragged: false, + ..Default::default() } } @@ -36,19 +54,44 @@ impl Node { radius: self.radius * zoom, color: self.color, - selected: self.selected, dragged: self.dragged, + selected: self.selected, + selected_child: self.selected_child, + selected_parent: self.selected_parent, + data: self.data.clone(), } } + + pub fn selected_child(&self) -> bool { + self.selected_child + } + + pub fn selected_parent(&self) -> bool { + self.selected_parent + } + + pub fn radius(&self) -> f32 { + self.radius + } + + pub fn selected(&self) -> bool { + self.selected || self.selected_child || self.selected_parent + } + + pub fn reset_precalculated(&mut self) { + self.radius = 5.; + self.selected_child = false; + self.selected_parent = false; + } } /// Stores properties of an edge that can be changed. Used to apply changes to the graph. #[derive(Clone, Debug, Copy, PartialEq)] pub struct Edge { /// Client data - pub data: E, + pub data: Option, pub width: f32, pub tip_size: f32, @@ -58,20 +101,36 @@ pub struct Edge { /// If `color` is None default color is used. pub color: Option, pub selected: bool, + + /// This field is recomputed on every frame + pub(crate) selected_child: bool, + /// This field is recomputed on every frame + pub(crate) selected_parent: bool, } -impl Edge { - pub fn new(data: E) -> Self { +impl Default for Edge { + fn default() -> Self { Self { - data, - width: 2., tip_size: 15., tip_angle: std::f32::consts::TAU / 50., curve_size: 20., - color: None, - selected: false, + data: Default::default(), + color: Default::default(), + selected: Default::default(), + selected_child: Default::default(), + selected_parent: Default::default(), + } + } +} + +impl Edge { + pub fn new(data: E) -> Self { + Self { + data: Some(data), + + ..Default::default() } } @@ -85,7 +144,27 @@ impl Edge { tip_angle: self.tip_angle, selected: self.selected, + selected_child: self.selected_child, + selected_parent: self.selected_parent, + data: self.data.clone(), } } + + pub fn selected_child(&self) -> bool { + self.selected_child + } + + pub fn selected_parent(&self) -> bool { + self.selected_parent + } + + pub fn selected(&self) -> bool { + self.selected || self.selected_child || self.selected_parent + } + + pub(crate) fn reset_precalculated(&mut self) { + self.selected_child = false; + self.selected_parent = false; + } } diff --git a/src/frame_state.rs b/src/frame_state.rs index 218e2e2..fcd5a29 100644 --- a/src/frame_state.rs +++ b/src/frame_state.rs @@ -1,39 +1,38 @@ use std::collections::HashMap; -use petgraph::stable_graph::{NodeIndex, StableGraph}; +use petgraph::stable_graph::{EdgeIndex, NodeIndex, StableGraph}; -use crate::{Edge, Node}; +use crate::{selections::Selections, Edge, Node}; + +pub(crate) type EdgesByNodes = Vec<((usize, usize), Vec<(EdgeIndex, Edge)>)>; /// `FrameState` is a utility struct for managing ephemerial state which is created and destroyed in one frame. /// /// The struct stores the selected nodes, dragged node, and cached edges by nodes. #[derive(Debug, Clone)] -pub struct FrameState { - pub selected: Vec, +pub(crate) struct FrameState { pub dragged: Option, - edges_by_nodes: Option>)>>, + pub selections: Option, + edges_by_nodes: Option>, } impl Default for FrameState { fn default() -> Self { Self { - selected: Vec::new(), - dragged: None, - edges_by_nodes: None, + dragged: Default::default(), + edges_by_nodes: Default::default(), + selections: Default::default(), } } } impl FrameState { + /// Helper method to get the edges by nodes. This is cached for performance. pub fn edges_by_nodes( &mut self, g: &StableGraph, Edge>, - ) -> &Vec<((usize, usize), Vec>)> { - if self.edges_by_nodes.is_some() { - return self.edges_by_nodes.as_ref().unwrap(); - } - - let mut edge_map: HashMap<(usize, usize), Vec>> = HashMap::new(); + ) -> &EdgesByNodes { + let mut edge_map: HashMap<(usize, usize), Vec<(EdgeIndex, Edge)>> = HashMap::new(); for edge_idx in g.edge_indices() { let (source_idx, target_idx) = g.edge_endpoints(edge_idx).unwrap(); @@ -44,7 +43,7 @@ impl FrameState { edge_map .entry((source, target)) .or_insert_with(Vec::new) - .push(edge); + .push((edge_idx, edge)); } let res = edge_map @@ -57,60 +56,3 @@ impl FrameState { self.edges_by_nodes.as_ref().unwrap() } } - -#[cfg(test)] -mod tests { - use super::*; - use egui::Vec2; - use petgraph::stable_graph::StableGraph; - - // Helper function to create a test StableGraph - fn create_test_graph() -> StableGraph, Edge> { - let mut graph = StableGraph::, Edge>::new(); - let n0 = graph.add_node(Node::new(Vec2::default(), ())); - let n1 = graph.add_node(Node::new(Vec2::default(), ())); - let n2 = graph.add_node(Node::new(Vec2::default(), ())); - - graph.add_edge(n0, n1, Edge::new(1)); - graph.add_edge(n0, n2, Edge::new(2)); - graph.add_edge(n1, n2, Edge::new(3)); - - graph - } - - #[test] - fn test_frame_state_default() { - let frame_state: FrameState = FrameState::default(); - assert_eq!(frame_state.selected.len(), 0); - assert!(frame_state.dragged.is_none()); - assert!(frame_state.edges_by_nodes.is_none()); - } - - #[test] - fn test_edges_by_nodes() { - let graph = create_test_graph(); - let mut frame_state = FrameState::::default(); - let edges_by_nodes = frame_state.edges_by_nodes(&graph); - - // Verify the size of the output vector - assert_eq!(edges_by_nodes.len(), 3); - - // Verify that edges_by_nodes contains the correct edges - let mut found_edges = HashMap::new(); - for ((source, target), edges) in edges_by_nodes { - found_edges.insert((*source, *target), edges); - } - - for edge_idx in graph.edge_indices() { - let (source_idx, target_idx) = graph.edge_endpoints(edge_idx).unwrap(); - let source = source_idx.index(); - let target = target_idx.index(); - let edge = graph.edge_weight(edge_idx).unwrap(); - - assert_eq!( - found_edges.get(&(source, target)).unwrap().first().unwrap(), - edge - ); - } - } -} diff --git a/src/graph_view.rs b/src/graph_view.rs index f12c98f..f995ce9 100644 --- a/src/graph_view.rs +++ b/src/graph_view.rs @@ -8,6 +8,7 @@ use crate::{ elements::Node, frame_state::FrameState, metadata::Metadata, + selections::Selections, settings::{SettingsInteraction, SettingsStyle}, Edge, SettingsNavigation, }; @@ -37,7 +38,7 @@ use petgraph::{ /// properties of the nodes or edges. pub struct GraphView<'a, N: Clone, E: Clone> { g: &'a mut StableGraph, Edge>, - setings_interaction: SettingsInteraction, + settings_interaction: SettingsInteraction, setings_navigation: SettingsNavigation, settings_style: SettingsStyle, changes_sender: Option<&'a Sender>, @@ -51,11 +52,13 @@ impl<'a, N: Clone, E: Clone> Widget for &mut GraphView<'a, N, E> { self.fit_if_first(&resp, &mut meta); - let mut state = self.draw_and_get_state(&p, &mut meta); + let mut frame_state = self.precompute_state(); - self.handle_nodes_drags(&resp, &mut state, &mut meta); - self.handle_click(&resp, &mut state, &mut meta); - self.handle_navigation(ui, &resp, &state, &mut meta); + self.draw(&p, &mut frame_state, &mut meta); + + self.handle_nodes_drags(&resp, &mut frame_state, &mut meta); + self.handle_click(&resp, &mut frame_state, &mut meta); + self.handle_navigation(ui, &resp, &frame_state, &mut meta); meta.store(ui); ui.ctx().request_repaint(); @@ -72,7 +75,7 @@ impl<'a, N: Clone, E: Clone> GraphView<'a, N, E> { g, settings_style: Default::default(), - setings_interaction: Default::default(), + settings_interaction: Default::default(), setings_navigation: Default::default(), changes_sender: Default::default(), } @@ -80,13 +83,13 @@ impl<'a, N: Clone, E: Clone> GraphView<'a, N, E> { /// Makes widget interactive sending changes. Events which /// are configured in `settings_interaction` are sent to the channel as soon as the occured. - pub fn with_interactions( - mut self, - settings_interaction: &SettingsInteraction, - changes_sender: &'a Sender, - ) -> Self { + pub fn with_interactions(mut self, settings_interaction: &SettingsInteraction) -> Self { + self.settings_interaction = settings_interaction.clone(); + self + } + + pub fn with_changes(mut self, changes_sender: &'a Sender) -> Self { self.changes_sender = Some(changes_sender); - self.setings_interaction = settings_interaction.clone(); self } @@ -159,9 +162,9 @@ impl<'a, N: Clone, E: Clone> GraphView<'a, N, E> { return; } - let clickable = self.setings_interaction.node_click - || self.setings_interaction.node_select - || self.setings_interaction.node_multiselect; + let clickable = self.settings_interaction.node_click + || self.settings_interaction.node_select + || self.settings_interaction.node_multiselect; if !(clickable) { return; @@ -171,9 +174,9 @@ impl<'a, N: Clone, E: Clone> GraphView<'a, N, E> { let node = self.node_by_pos(meta, resp.hover_pos().unwrap()); if node.is_none() { let selectable = - self.setings_interaction.node_select || self.setings_interaction.node_multiselect; + self.settings_interaction.node_select || self.settings_interaction.node_multiselect; if selectable { - self.deselect_all_nodes(state); + self.deselect_all(state); } return; } @@ -182,7 +185,7 @@ impl<'a, N: Clone, E: Clone> GraphView<'a, N, E> { } fn handle_node_click(&mut self, idx: NodeIndex, state: &FrameState) { - if !self.setings_interaction.node_select { + if !self.settings_interaction.node_select { self.click_node(idx); return; } @@ -193,8 +196,8 @@ impl<'a, N: Clone, E: Clone> GraphView<'a, N, E> { return; } - if !self.setings_interaction.node_multiselect { - self.deselect_all_nodes(state); + if !self.settings_interaction.node_multiselect { + self.deselect_all(state); } self.select_node(idx); @@ -206,7 +209,7 @@ impl<'a, N: Clone, E: Clone> GraphView<'a, N, E> { state: &mut FrameState, meta: &mut Metadata, ) { - if !self.setings_interaction.node_drag { + if !self.settings_interaction.node_drag { return; } @@ -319,7 +322,7 @@ impl<'a, N: Clone, E: Clone> GraphView<'a, N, E> { self.g.node_weight_mut(idx).unwrap().selected = true; let mut changes = Changes::default(); - changes.set_selected(idx, true); + changes.select_node(idx); self.send_changes(changes); } @@ -327,21 +330,33 @@ impl<'a, N: Clone, E: Clone> GraphView<'a, N, E> { self.g.node_weight_mut(idx).unwrap().selected = false; let mut changes = Changes::default(); - changes.set_selected(idx, false); + changes.deselect_node(idx); self.send_changes(changes); } - fn deselect_all_nodes(&mut self, state: &FrameState) { - if state.selected.is_empty() { + fn deselect_all(&mut self, state: &FrameState) { + if state.selections.is_none() { return; } + let (selected_nodes, selected_edges) = state.selections.as_ref().unwrap().elements(); + let mut changes = Changes::default(); - state.selected.iter().for_each(|idx| { + selected_nodes.iter().for_each(|idx| { self.g.node_weight_mut(*idx).unwrap().selected = false; - changes.set_selected(*idx, false); + self.g.node_weight_mut(*idx).unwrap().selected_child = false; + self.g.node_weight_mut(*idx).unwrap().selected_parent = false; + changes.deselect_node(*idx); + }); + if !changes.is_empty() { + self.send_changes(changes); + } + + selected_edges.iter().for_each(|idx| { + self.g.edge_weight_mut(*idx).unwrap().selected = false; + self.g.edge_weight_mut(*idx).unwrap().selected_child = false; + self.g.edge_weight_mut(*idx).unwrap().selected_parent = false; }); - self.send_changes(changes); } fn set_dragged(&mut self, idx: NodeIndex, val: bool) { @@ -371,16 +386,20 @@ impl<'a, N: Clone, E: Clone> GraphView<'a, N, E> { self.send_changes(changes); } - fn draw_and_get_state(&mut self, p: &Painter, metadata: &mut Metadata) -> FrameState { - let mut frame_state = FrameState::default(); + fn precompute_state(&mut self) -> FrameState { + let mut state = FrameState::default(); - // reset node radius - let default_radius = Node::new(Vec2::default(), ()).radius; + // reset nodes radiuses self.g .node_weights_mut() - .for_each(|n| n.radius = default_radius); + .for_each(|n| n.reset_precalculated()); + + self.g + .edge_weights_mut() + .for_each(|e| e.reset_precalculated()); - let edges = frame_state.edges_by_nodes(self.g); + // compute nodes radiuses + let edges = state.edges_by_nodes(self.g); edges.iter().for_each(|((start, end), edges)| { self.g .node_weight_mut(NodeIndex::new(*start)) @@ -390,13 +409,58 @@ impl<'a, N: Clone, E: Clone> GraphView<'a, N, E> { self.settings_style.edge_radius_weight * edges.len() as f32; }); - let edges_shapes = self.draw_edges(p, &mut frame_state, metadata); - let nodes_shapes = self.draw_nodes(p, metadata, &mut frame_state); + // compute selections + let mut selections = Selections::default(); + let mut subselected_nodes = vec![]; + let mut subselected_edges = vec![]; + self.g.node_references().for_each(|(root_idx, root_n)| { + if !root_n.selected { + return; + } + + selections.add_selection(self.g, root_idx, self.settings_interaction.selection_depth); + + let elements = selections.elements_by_root(root_idx); + if elements.is_none() { + return; + } + + let (nodes, edges) = elements.unwrap(); + + nodes.iter().for_each(|idx| { + if *idx == root_idx { + return; + } + subselected_nodes.push(*idx); + }); + + edges.iter().for_each(|idx| subselected_edges.push(*idx)); + }); + state.selections = Some(selections); + + subselected_nodes.iter().for_each(|idx| { + match self.settings_interaction.selection_depth > 0 { + true => self.g.node_weight_mut(*idx).unwrap().selected_child = true, + false => self.g.node_weight_mut(*idx).unwrap().selected_parent = true, + } + }); + + subselected_edges.iter().for_each(|idx| { + match self.settings_interaction.selection_depth > 0 { + true => self.g.edge_weight_mut(*idx).unwrap().selected_child = true, + false => self.g.edge_weight_mut(*idx).unwrap().selected_parent = true, + } + }); + + state + } + + fn draw(&mut self, p: &Painter, state: &mut FrameState, metadata: &mut Metadata) { + let edges_shapes = self.draw_edges(p, state, metadata); + let nodes_shapes = self.draw_nodes(p, metadata, state); self.draw_edges_shapes(p, edges_shapes); self.draw_nodes_shapes(p, nodes_shapes); - - frame_state } fn draw_nodes( @@ -436,10 +500,6 @@ impl<'a, N: Clone, E: Clone> GraphView<'a, N, E> { frame_state.dragged = Some(idx); } - if n.selected { - frame_state.selected.push(idx) - } - let selected = self.draw_node(p, n, meta); shapes.extend(selected); }); @@ -461,7 +521,7 @@ impl<'a, N: Clone, E: Clone> GraphView<'a, N, E> { .iter() .for_each(|((start, end), edges)| { let mut order = edges.len(); - edges.iter().enumerate().for_each(|(_, e)| { + edges.iter().for_each(|(_, e)| { order -= 1; let edge = e.screen_transform(meta.zoom); @@ -553,10 +613,6 @@ impl<'a, N: Clone, E: Clone> GraphView<'a, N, E> { e: &Edge, order: usize, ) -> Vec { - let color = match e.color { - Some(color) => color, - None => self.settings_style.color_edge(p.ctx()), - }; let pos_start_and_end = n.location.to_pos2(); let loop_size = n.radius * (4. + 1. + order as f32); @@ -569,7 +625,7 @@ impl<'a, N: Clone, E: Clone> GraphView<'a, N, E> { pos_start_and_end.y - loop_size, ); - let stroke = Stroke::new(e.width, color); + let stroke = Stroke::new(e.width, self.settings_style.color_edge(p.ctx(), e)); let shape_basic = CubicBezierShape::from_points_stroke( [ pos_start_and_end, @@ -582,13 +638,17 @@ impl<'a, N: Clone, E: Clone> GraphView<'a, N, E> { stroke, ); - if !e.selected { + if !e.selected() { p.add(shape_basic); return vec![]; } let mut shapes = vec![shape_basic]; - let highlighted_stroke = Stroke::new(e.width * 2., self.settings_style.color_highlight); + + let highlighted_stroke = Stroke::new( + e.width * 2., + self.settings_style.color_edge_highlight(e).unwrap(), + ); shapes.push(CubicBezierShape::from_points_stroke( [ pos_start_and_end, @@ -612,10 +672,6 @@ impl<'a, N: Clone, E: Clone> GraphView<'a, N, E> { e: &Edge, order: usize, ) -> (Vec, Vec) { - let color = match e.color { - Some(color) => color, - None => self.settings_style.color_edge(p.ctx()), - }; let pos_start = n_start.location.to_pos2(); let pos_end = n_end.location.to_pos2(); @@ -629,8 +685,7 @@ impl<'a, N: Clone, E: Clone> GraphView<'a, N, E> { let tip_point = pos_start + vec - end_node_radius_vec; let start_point = pos_start + start_node_radius_vec; - let stroke = Stroke::new(e.width, color); - let highlighted_stroke = Stroke::new(e.width * 2., self.settings_style.color_highlight); + let stroke = Stroke::new(e.width, self.settings_style.color_edge(p.ctx(), e)); // draw straight edge if order == 0 { @@ -642,7 +697,7 @@ impl<'a, N: Clone, E: Clone> GraphView<'a, N, E> { shapes.push(Shape::line_segment([tip_point, head_point_1], stroke)); shapes.push(Shape::line_segment([tip_point, head_point_2], stroke)); - if !e.selected { + if !e.selected() { shapes.into_iter().for_each(|shape| { p.add(shape); }); @@ -650,6 +705,10 @@ impl<'a, N: Clone, E: Clone> GraphView<'a, N, E> { return (vec![], vec![]); } + let highlighted_stroke = Stroke::new( + e.width * 2., + self.settings_style.color_edge_highlight(e).unwrap(), + ); shapes.push(Shape::line_segment( [start_point, tip_point], highlighted_stroke, @@ -703,6 +762,10 @@ impl<'a, N: Clone, E: Clone> GraphView<'a, N, E> { return (vec![], vec![]); } + let highlighted_stroke = Stroke::new( + e.width * 2., + self.settings_style.color_edge_highlight(e).unwrap(), + ); quadratic_shapes.push(QuadraticBezierShape::from_points_stroke( [start_point, control_point, tip_point], false, @@ -732,16 +795,18 @@ impl<'a, N: Clone, E: Clone> GraphView<'a, N, E> { } fn draw_node_basic(&self, loc: Pos2, p: &Painter, node: &Node) -> Vec { - let color = match node.color { - Some(c) => c, - None => self.settings_style.color_node(p.ctx()), - }; - - if !(node.selected || node.dragged) { - p.circle_filled(loc, node.radius, color); + let color = self.settings_style.color_node(p.ctx(), node); + if !(node.selected() || node.dragged) { + // draw the node in place + p.circle_filled( + loc, + node.radius, + self.settings_style.color_node(p.ctx(), node), + ); return vec![]; } + // draw the node later if it's selected or dragged to make sure it's on top vec![CircleShape { center: loc, radius: node.radius, @@ -751,32 +816,23 @@ impl<'a, N: Clone, E: Clone> GraphView<'a, N, E> { } fn draw_node_interacted(&self, loc: Pos2, node: &Node) -> Vec { - if !(node.selected || node.dragged) { + if !(node.selected() || node.dragged) { return vec![]; } let mut shapes = vec![]; let highlight_radius = node.radius * 1.5; - // draw a border around the selected node - if node.selected { - shapes.push(CircleShape { - center: loc, - radius: highlight_radius, - fill: Color32::TRANSPARENT, - stroke: Stroke::new(node.radius, self.settings_style.color_highlight), - }); - }; + shapes.push(CircleShape { + center: loc, + radius: highlight_radius, + fill: Color32::TRANSPARENT, + stroke: Stroke::new( + node.radius, + self.settings_style.color_node_highlight(node).unwrap(), + ), + }); - // draw a border around the dragged node - if node.dragged { - shapes.push(CircleShape { - center: loc, - radius: highlight_radius, - fill: Color32::TRANSPARENT, - stroke: Stroke::new(node.radius, self.settings_style.color_drag), - }); - } shapes } diff --git a/src/lib.rs b/src/lib.rs index 235f5b0..cbec388 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,10 @@ mod changes; mod elements; +mod frame_state; mod graph_view; mod metadata; +mod selections; mod settings; -mod frame_state; pub use self::changes::{Changes, ChangesNode}; pub use self::elements::{Edge, Node}; diff --git a/src/selections.rs b/src/selections.rs new file mode 100644 index 0000000..accb62b --- /dev/null +++ b/src/selections.rs @@ -0,0 +1,168 @@ +use egui::epaint::ahash::HashMap; +use petgraph::{ + stable_graph::{EdgeIndex, NodeIndex, StableGraph}, + visit::EdgeRef, + Direction, Graph, +}; + +use crate::{Edge, Node}; + +pub(crate) type Selection = Graph; +pub(crate) type Elements = (Vec, Vec); + +#[derive(Default, Debug, Clone)] +pub(crate) struct Selections { + data: HashMap, +} + +impl Selections { + pub fn elements(&self) -> Elements { + let mut nodes = vec![]; + let mut edges = vec![]; + + for (root, _) in self.data.iter() { + let (curr_nodes, curr_edges) = self.elements_by_root(*root).unwrap(); + nodes.extend(curr_nodes); + edges.extend(curr_edges); + } + + (nodes, edges) + } + + /// Walks the entire graph and collect node weights to `nodes` + /// and edges weights to `edges` + pub fn elements_by_root(&self, root: NodeIndex) -> Option { + let g = self.data.get(&root)?; + + if g.node_count() == 0 { + return Some((vec![root], vec![])); + } + + Some(( + g.node_weights().cloned().collect(), + g.edge_weights().cloned().collect(), + )) + } + + pub fn add_selection( + &mut self, + g: &StableGraph, Edge>, + root: NodeIndex, + depth: i32, + ) { + let mut selection_g = Graph::::new(); + if depth == 0 { + self.data.insert(root, selection_g); + return; + } + + let dir = match depth > 0 { + true => petgraph::Direction::Outgoing, + false => petgraph::Direction::Incoming, + }; + + self.collect_generations( + g, + &mut selection_g, + root, + depth.unsigned_abs() as usize, + dir, + ); + + self.data.insert(root, selection_g); + } + + fn collect_generations( + &self, + g: &StableGraph, Edge>, + selection_g: &mut Graph, + root: NodeIndex, + n: usize, + dir: Direction, + ) { + if n == 0 { + return; + } + + let mut depth = n; + let mut next_start = vec![root]; + while depth > 0 { + depth -= 1; + + let mut next_next_start = vec![]; + next_start.iter().for_each(|g_idx| { + let s_idx = selection_g.add_node(*g_idx); + g.edges_directed(*g_idx, dir).for_each(|edge| { + let next = match dir { + Direction::Incoming => edge.source(), + Direction::Outgoing => edge.target(), + }; + let next_idx = selection_g.add_node(next); + selection_g.add_edge(s_idx, next_idx, edge.id()); + next_next_start.push(next); + }); + }); + + next_start = next_next_start; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use egui::Vec2; + use petgraph::stable_graph::StableGraph; + + // Helper function to create a test StableGraph + fn create_test_graph() -> StableGraph, Edge> { + let mut graph = StableGraph::, Edge>::new(); + let n0 = graph.add_node(Node::new(Vec2::default(), ())); + let n1 = graph.add_node(Node::new(Vec2::default(), ())); + let n2 = graph.add_node(Node::new(Vec2::default(), ())); + + graph.add_edge(n0, n1, Edge::new(1)); + graph.add_edge(n0, n2, Edge::new(2)); + graph.add_edge(n1, n2, Edge::new(3)); + + graph + } + + #[test] + fn test_selections_add_and_elements() { + let graph = create_test_graph(); + let mut selections = Selections::default(); + + selections.add_selection(&graph, NodeIndex::new(0), 1); + + let (nodes, edges) = selections.elements(); + assert_eq!(nodes.len(), 3); + assert_eq!(edges.len(), 2); + + assert!(nodes.contains(&NodeIndex::new(0))); + assert!(nodes.contains(&NodeIndex::new(1))); + assert!(nodes.contains(&NodeIndex::new(2))); + + assert!(edges.contains(&EdgeIndex::new(0))); + assert!(edges.contains(&EdgeIndex::new(1))); + } + + #[test] + fn test_elements_by_root() { + let graph = create_test_graph(); + let mut selections = Selections::default(); + + selections.add_selection(&graph, NodeIndex::new(0), 1); + + let (nodes, edges) = selections.elements_by_root(NodeIndex::new(0)).unwrap(); + assert_eq!(nodes.len(), 3); + assert_eq!(edges.len(), 2); + + assert!(nodes.contains(&NodeIndex::new(0))); + assert!(nodes.contains(&NodeIndex::new(1))); + assert!(nodes.contains(&NodeIndex::new(2))); + + assert!(edges.contains(&EdgeIndex::new(0))); + assert!(edges.contains(&EdgeIndex::new(1))); + } +} diff --git a/src/settings.rs b/src/settings.rs index 63ae176..a11aa40 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,5 +1,7 @@ use egui::Color32; +use crate::{Edge, Node}; + /// `SettingsInteraction` stores settings for the interaction with the graph. /// /// `node_click` is included in `node_select` and `node_multiselect`. @@ -17,6 +19,12 @@ pub struct SettingsInteraction { /// Clicking on empty space deselects all nodes. pub node_select: bool, + /// How deep into the neighbours of selected nodes should the selection go. + /// `selection_depth == 0` means only selected nodes are selected. + /// `selection_depth > 0` means children of selected nodes are selected up to `selection_depth` generation. + /// `selection_depth < 0` means parents of selected nodes are selected up to `selection_depth` generation. + pub selection_depth: i32, + /// Multiselection for nodes, enables node_click and node_select. pub node_multiselect: bool, } @@ -60,7 +68,16 @@ pub struct SettingsStyle { /// For every edge connected to node its radius is getting bigger by this value. pub edge_radius_weight: f32, - pub color_highlight: Color32, + /// Used to color children of the selected nodes. + pub color_selection_child: Color32, + + /// Used to color parents of the selected nodes. + pub color_selection_parent: Color32, + + /// Used to color selected nodes. + pub color_selection: Color32, + + /// Color of nodes being dragged. pub color_drag: Color32, } @@ -68,16 +85,22 @@ impl Default for SettingsStyle { fn default() -> Self { Self { edge_radius_weight: 1., + color_selection: Color32::from_rgba_unmultiplied(0, 255, 127, 153), // Spring Green + color_selection_child: Color32::from_rgba_unmultiplied(100, 149, 237, 153), // Cornflower Blue + color_selection_parent: Color32::from_rgba_unmultiplied(255, 105, 180, 153), // Hot Pink color_node: Color32::from_rgb(200, 200, 200), // Light Gray color_edge: Color32::from_rgb(128, 128, 128), // Gray - color_highlight: Color32::from_rgba_unmultiplied(100, 149, 237, 153), // Cornflower Blue color_drag: Color32::from_rgba_unmultiplied(240, 128, 128, 153), // Light Coral } } } impl SettingsStyle { - pub fn color_node(&self, ctx: &egui::Context) -> Color32 { + pub(crate) fn color_node(&self, ctx: &egui::Context, n: &Node) -> Color32 { + if n.color.is_some() { + return n.color.unwrap(); + } + if ctx.style().visuals.dark_mode { return self.color_node; } @@ -85,10 +108,50 @@ impl SettingsStyle { self.color_edge } - pub fn color_edge(&self, ctx: &egui::Context) -> Color32 { + pub(crate) fn color_node_highlight(&self, n: &Node) -> Option { + if n.dragged { + return Some(self.color_drag); + } + + if n.selected { + return Some(self.color_selection); + } + + if n.selected_child { + return Some(self.color_selection_child); + } + + if n.selected_parent { + return Some(self.color_selection_parent); + } + + None + } + + pub fn color_edge(&self, ctx: &egui::Context, e: &Edge) -> Color32 { + if e.color.is_some() { + return e.color.unwrap(); + } + if ctx.style().visuals.dark_mode { return self.color_edge; } self.color_node } + + pub fn color_edge_highlight(&self, e: &Edge) -> Option { + if e.selected { + return Some(self.color_selection); + } + + if e.selected_child { + return Some(self.color_selection_child); + } + + if e.selected_parent { + return Some(self.color_selection_parent); + } + + None + } }