From 006a5f360850a479eeff0ab5ece1122a132e724a Mon Sep 17 00:00:00 2001 From: Kamil Koczurek Date: Wed, 10 May 2023 11:54:21 +0200 Subject: [PATCH 1/2] node finder: support node categories * Add CategoryTrait that gives a name to a category of nodes. Provide implementations for String, &str and (). * Add NodeTemplateTrait::node_finder_categories to list categories to which the template belongs, defaults to an empty vector. * Node finder now displays at the top a collapsible header for each category and corresponding nodes inside of them. Nodes without a category are displayed below, so the behavior of programs that don't adapt to use categories remains the same. The collapsible headers behave as follows: - For empty query, all categories are collapsed - Otherwise: 1. Categories that contain no matching nodes are ommited. 2. Matching categories are now opened by default -- it's assumed that filtered results are more manageable and can now be unwrapped. 3. The user can normally un/collapse each category, but if the query is changed, the default is applied. * Update the example to use this feature. This change will break all existing programs and the simplest fix to that is adding `type CategoryType = ();` in the implementation of `NodeTemplateTrait`. Default associated types are not supported, so this can't be avoided with this design. --- egui_node_graph/src/editor_ui.rs | 4 +- egui_node_graph/src/node_finder.rs | 82 ++++++++++++++++++++++++------ egui_node_graph/src/traits.rs | 47 +++++++++++++++++ egui_node_graph_example/src/app.rs | 20 ++++++-- 4 files changed, 134 insertions(+), 19 deletions(-) diff --git a/egui_node_graph/src/editor_ui.rs b/egui_node_graph/src/editor_ui.rs index d009e42..d018b40 100644 --- a/egui_node_graph/src/editor_ui.rs +++ b/egui_node_graph/src/editor_ui.rs @@ -83,7 +83,7 @@ pub struct GraphNodeWidget<'a, NodeData, DataType, ValueType> { pub pan: egui::Vec2, } -impl +impl GraphEditorState where NodeData: NodeDataTrait< @@ -100,8 +100,10 @@ where DataType = DataType, ValueType = ValueType, UserState = UserState, + CategoryType = CategoryType, >, DataType: DataTypeTrait, + CategoryType: CategoryTrait, { #[must_use] pub fn draw_graph_editor( diff --git a/egui_node_graph/src/node_finder.rs b/egui_node_graph/src/node_finder.rs index f0863f5..c0a6b2f 100644 --- a/egui_node_graph/src/node_finder.rs +++ b/egui_node_graph/src/node_finder.rs @@ -1,6 +1,6 @@ -use std::marker::PhantomData; +use std::{collections::BTreeMap, marker::PhantomData}; -use crate::{color_hex_utils::*, NodeTemplateIter, NodeTemplateTrait}; +use crate::{color_hex_utils::*, CategoryTrait, NodeTemplateIter, NodeTemplateTrait}; use egui::*; @@ -14,9 +14,11 @@ pub struct NodeFinder { _phantom: PhantomData, } -impl NodeFinder +impl NodeFinder where - NodeTemplate: NodeTemplateTrait, + NodeTemplate: + NodeTemplateTrait, + CategoryType: CategoryTrait, { pub fn new_at(pos: Pos2) -> Self { NodeFinder { @@ -62,12 +64,29 @@ where resp.request_focus(); self.just_spawned = false; } + let update_open = resp.changed(); let mut query_submit = resp.lost_focus() && ui.input(|i| i.key_pressed(Key::Enter)); let max_height = ui.input(|i| i.screen_rect.height() * 0.5); let scroll_area_width = resp.rect.width() - 30.0; + let all_kinds = all_kinds.all_kinds(); + let mut categories: BTreeMap> = Default::default(); + let mut orphan_kinds = Vec::new(); + + for kind in &all_kinds { + let kind_categories = kind.node_finder_categories(user_state); + + if kind_categories.is_empty() { + orphan_kinds.push(kind); + } else { + for category in kind_categories { + categories.entry(category.name()).or_default().push(kind); + } + } + } + Frame::default() .inner_margin(vec2(10.0, 10.0)) .show(ui, |ui| { @@ -75,18 +94,51 @@ where .max_height(max_height) .show(ui, |ui| { ui.set_width(scroll_area_width); - for kind in all_kinds.all_kinds() { + for (category, kinds) in categories { + let filtered_kinds: Vec<_> = kinds + .into_iter() + .map(|kind| { + let kind_name = + kind.node_finder_label(user_state).to_string(); + (kind, kind_name) + }) + .filter(|(_kind, kind_name)| { + kind_name + .to_lowercase() + .contains(self.query.to_lowercase().as_str()) + }) + .collect(); + + if !filtered_kinds.is_empty() { + let default_open = !self.query.is_empty(); + + CollapsingHeader::new(&category) + .default_open(default_open) + .open(update_open.then_some(default_open)) + .show(ui, |ui| { + for (kind, kind_name) in filtered_kinds { + if ui + .selectable_label(false, kind_name) + .clicked() + { + submitted_archetype = Some(kind.clone()); + } else if query_submit { + submitted_archetype = Some(kind.clone()); + query_submit = false; + } + } + }); + } + } + + for kind in orphan_kinds { let kind_name = kind.node_finder_label(user_state).to_string(); - if kind_name - .to_lowercase() - .contains(self.query.to_lowercase().as_str()) - { - if ui.selectable_label(false, kind_name).clicked() { - submitted_archetype = Some(kind); - } else if query_submit { - submitted_archetype = Some(kind); - query_submit = false; - } + + if ui.selectable_label(false, kind_name).clicked() { + submitted_archetype = Some(kind.clone()); + } else if query_submit { + submitted_archetype = Some(kind.clone()); + query_submit = false; } } }); diff --git a/egui_node_graph/src/traits.rs b/egui_node_graph/src/traits.rs index 72a81e6..9499d08 100644 --- a/egui_node_graph/src/traits.rs +++ b/egui_node_graph/src/traits.rs @@ -176,6 +176,38 @@ pub trait NodeTemplateIter { fn all_kinds(&self) -> Vec; } +/// Describes a category of nodes. +/// +/// Used by [`NodeTemplateTrait::node_finder_categories`] to categorize nodes +/// templates into groups. +/// +/// If all nodes in a program are known beforehand, it's usefult to define +/// an enum containing all categories and implement [`CategoryTrait`] for it. This will +/// make it impossible to accidentally create a new category by mis-typing an existing +/// one, like in the case of using string types. +pub trait CategoryTrait { + /// Name of the category. + fn name(&self) -> String; +} + +impl CategoryTrait for () { + fn name(&self) -> String { + String::new() + } +} + +impl<'a> CategoryTrait for &'a str { + fn name(&self) -> String { + self.to_string() + } +} + +impl CategoryTrait for String { + fn name(&self) -> String { + self.clone() + } +} + /// This trait must be implemented by the `NodeTemplate` generic parameter of /// the [`GraphEditorState`]. It allows the customization of node templates. A /// node template is what describes what kinds of nodes can be added to the @@ -189,6 +221,12 @@ pub trait NodeTemplateTrait: Clone { type ValueType; /// Must be set to the custom user `UserState` type type UserState; + /// Must be a type that implements the [`CategoryTrait`] trait. + /// + /// `&'static str` is a good default if you intend to simply type out + /// the categories of your node. Use `()` if you don't need categories + /// at all. + type CategoryType; /// Returns a descriptive name for the node kind, used in the node finder. /// @@ -197,6 +235,15 @@ pub trait NodeTemplateTrait: Clone { /// more information fn node_finder_label(&self, user_state: &mut Self::UserState) -> std::borrow::Cow; + /// Vec of categories to which the node belongs. + /// + /// It's often useful to organize similar nodes into categories, which will + /// then be used by the node finder to show a more manageable UI, especially + /// if the node template are numerous. + fn node_finder_categories(&self, _user_state: &mut Self::UserState) -> Vec { + Vec::default() + } + /// Returns a descriptive name for the node kind, used in the graph. fn node_graph_label(&self, user_state: &mut Self::UserState) -> String; diff --git a/egui_node_graph_example/src/app.rs b/egui_node_graph_example/src/app.rs index 5ad68ca..ddef9f8 100644 --- a/egui_node_graph_example/src/app.rs +++ b/egui_node_graph_example/src/app.rs @@ -71,13 +71,13 @@ impl MyValueType { #[derive(Clone, Copy)] #[cfg_attr(feature = "persistence", derive(serde::Serialize, serde::Deserialize))] pub enum MyNodeTemplate { - MakeVector, MakeScalar, AddScalar, SubtractScalar, - VectorTimesScalar, + MakeVector, AddVector, SubtractVector, + VectorTimesScalar, } /// The response type is used to encode side-effects produced when drawing a @@ -125,19 +125,33 @@ impl NodeTemplateTrait for MyNodeTemplate { type DataType = MyDataType; type ValueType = MyValueType; type UserState = MyGraphState; + type CategoryType = &'static str; fn node_finder_label(&self, _user_state: &mut Self::UserState) -> Cow<'_, str> { Cow::Borrowed(match self { - MyNodeTemplate::MakeVector => "New vector", MyNodeTemplate::MakeScalar => "New scalar", MyNodeTemplate::AddScalar => "Scalar add", MyNodeTemplate::SubtractScalar => "Scalar subtract", + MyNodeTemplate::MakeVector => "New vector", MyNodeTemplate::AddVector => "Vector add", MyNodeTemplate::SubtractVector => "Vector subtract", MyNodeTemplate::VectorTimesScalar => "Vector times scalar", }) } + // this is what allows the library to show collapsible lists in the node finder. + fn node_finder_categories(&self, _user_state: &mut Self::UserState) -> Vec<&'static str> { + match self { + MyNodeTemplate::MakeScalar + | MyNodeTemplate::AddScalar + | MyNodeTemplate::SubtractScalar => vec!["Scalar"], + MyNodeTemplate::MakeVector + | MyNodeTemplate::AddVector + | MyNodeTemplate::SubtractVector => vec!["Vector"], + MyNodeTemplate::VectorTimesScalar => vec!["Vector", "Scalar"], + } + } + fn node_graph_label(&self, user_state: &mut Self::UserState) -> String { // It's okay to delegate this to node_finder_label if you don't want to // show different names in the node finder and the node itself. From 671d1e6eed4f4744a71a62758f8c43a9ab7b5673 Mon Sep 17 00:00:00 2001 From: Kamil Koczurek Date: Wed, 10 May 2023 12:04:42 +0200 Subject: [PATCH 2/2] example: appease clippy in main.rs --- egui_node_graph_example/src/main.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/egui_node_graph_example/src/main.rs b/egui_node_graph_example/src/main.rs index 0fe95af..8d5d544 100644 --- a/egui_node_graph_example/src/main.rs +++ b/egui_node_graph_example/src/main.rs @@ -2,6 +2,8 @@ #![cfg_attr(not(debug_assertions), deny(warnings))] // Forbid warnings in release builds #![warn(clippy::all, rust_2018_idioms)] +use egui_node_graph_example::NodeGraphExample; + // When compiling natively: #[cfg(not(target_arch = "wasm32"))] fn main() { @@ -14,10 +16,10 @@ fn main() { cc.egui_ctx.set_visuals(Visuals::dark()); #[cfg(feature = "persistence")] { - Box::new(egui_node_graph_example::NodeGraphExample::new(cc)) + Box::new(NodeGraphExample::new(cc)) } #[cfg(not(feature = "persistence"))] - Box::new(egui_node_graph_example::NodeGraphExample::default()) + Box::::default() }), ) .expect("Failed to run native example");