Merge pull request #90 from kamirr/kek/categories

node categories
This commit is contained in:
setzer22 2023-05-15 11:23:11 +02:00 committed by GitHub
commit 943cd4b94c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 138 additions and 21 deletions

View File

@ -83,7 +83,7 @@ pub struct GraphNodeWidget<'a, NodeData, DataType, ValueType> {
pub pan: egui::Vec2, pub pan: egui::Vec2,
} }
impl<NodeData, DataType, ValueType, NodeTemplate, UserResponse, UserState> impl<NodeData, DataType, ValueType, NodeTemplate, UserResponse, UserState, CategoryType>
GraphEditorState<NodeData, DataType, ValueType, NodeTemplate, UserState> GraphEditorState<NodeData, DataType, ValueType, NodeTemplate, UserState>
where where
NodeData: NodeDataTrait< NodeData: NodeDataTrait<
@ -100,8 +100,10 @@ where
DataType = DataType, DataType = DataType,
ValueType = ValueType, ValueType = ValueType,
UserState = UserState, UserState = UserState,
CategoryType = CategoryType,
>, >,
DataType: DataTypeTrait<UserState>, DataType: DataTypeTrait<UserState>,
CategoryType: CategoryTrait,
{ {
#[must_use] #[must_use]
pub fn draw_graph_editor( pub fn draw_graph_editor(

View File

@ -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::*; use egui::*;
@ -14,9 +14,11 @@ pub struct NodeFinder<NodeTemplate> {
_phantom: PhantomData<NodeTemplate>, _phantom: PhantomData<NodeTemplate>,
} }
impl<NodeTemplate, NodeData, UserState> NodeFinder<NodeTemplate> impl<NodeTemplate, NodeData, UserState, CategoryType> NodeFinder<NodeTemplate>
where where
NodeTemplate: NodeTemplateTrait<NodeData = NodeData, UserState = UserState>, NodeTemplate:
NodeTemplateTrait<NodeData = NodeData, UserState = UserState, CategoryType = CategoryType>,
CategoryType: CategoryTrait,
{ {
pub fn new_at(pos: Pos2) -> Self { pub fn new_at(pos: Pos2) -> Self {
NodeFinder { NodeFinder {
@ -62,12 +64,29 @@ where
resp.request_focus(); resp.request_focus();
self.just_spawned = false; 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 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 max_height = ui.input(|i| i.screen_rect.height() * 0.5);
let scroll_area_width = resp.rect.width() - 30.0; let scroll_area_width = resp.rect.width() - 30.0;
let all_kinds = all_kinds.all_kinds();
let mut categories: BTreeMap<String, Vec<&NodeTemplate>> = 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() Frame::default()
.inner_margin(vec2(10.0, 10.0)) .inner_margin(vec2(10.0, 10.0))
.show(ui, |ui| { .show(ui, |ui| {
@ -75,19 +94,52 @@ where
.max_height(max_height) .max_height(max_height)
.show(ui, |ui| { .show(ui, |ui| {
ui.set_width(scroll_area_width); ui.set_width(scroll_area_width);
for kind in all_kinds.all_kinds() { for (category, kinds) in categories {
let kind_name = kind.node_finder_label(user_state).to_string(); let filtered_kinds: Vec<_> = kinds
if kind_name .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() .to_lowercase()
.contains(self.query.to_lowercase().as_str()) .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()
{ {
if ui.selectable_label(false, kind_name).clicked() { submitted_archetype = Some(kind.clone());
submitted_archetype = Some(kind);
} else if query_submit { } else if query_submit {
submitted_archetype = Some(kind); submitted_archetype = Some(kind.clone());
query_submit = false; query_submit = false;
} }
} }
});
}
}
for kind in orphan_kinds {
let kind_name = kind.node_finder_label(user_state).to_string();
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;
}
} }
}); });
}); });

View File

@ -194,6 +194,38 @@ pub trait NodeTemplateIter {
fn all_kinds(&self) -> Vec<Self::Item>; fn all_kinds(&self) -> Vec<Self::Item>;
} }
/// 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 /// This trait must be implemented by the `NodeTemplate` generic parameter of
/// the [`GraphEditorState`]. It allows the customization of node templates. A /// the [`GraphEditorState`]. It allows the customization of node templates. A
/// node template is what describes what kinds of nodes can be added to the /// node template is what describes what kinds of nodes can be added to the
@ -207,6 +239,12 @@ pub trait NodeTemplateTrait: Clone {
type ValueType; type ValueType;
/// Must be set to the custom user `UserState` type /// Must be set to the custom user `UserState` type
type UserState; 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. /// Returns a descriptive name for the node kind, used in the node finder.
/// ///
@ -215,6 +253,15 @@ pub trait NodeTemplateTrait: Clone {
/// more information /// more information
fn node_finder_label(&self, user_state: &mut Self::UserState) -> std::borrow::Cow<str>; fn node_finder_label(&self, user_state: &mut Self::UserState) -> std::borrow::Cow<str>;
/// 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<Self::CategoryType> {
Vec::default()
}
/// Returns a descriptive name for the node kind, used in the graph. /// Returns a descriptive name for the node kind, used in the graph.
fn node_graph_label(&self, user_state: &mut Self::UserState) -> String; fn node_graph_label(&self, user_state: &mut Self::UserState) -> String;

View File

@ -71,13 +71,13 @@ impl MyValueType {
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
#[cfg_attr(feature = "persistence", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "persistence", derive(serde::Serialize, serde::Deserialize))]
pub enum MyNodeTemplate { pub enum MyNodeTemplate {
MakeVector,
MakeScalar, MakeScalar,
AddScalar, AddScalar,
SubtractScalar, SubtractScalar,
VectorTimesScalar, MakeVector,
AddVector, AddVector,
SubtractVector, SubtractVector,
VectorTimesScalar,
} }
/// The response type is used to encode side-effects produced when drawing a /// 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 DataType = MyDataType;
type ValueType = MyValueType; type ValueType = MyValueType;
type UserState = MyGraphState; type UserState = MyGraphState;
type CategoryType = &'static str;
fn node_finder_label(&self, _user_state: &mut Self::UserState) -> Cow<'_, str> { fn node_finder_label(&self, _user_state: &mut Self::UserState) -> Cow<'_, str> {
Cow::Borrowed(match self { Cow::Borrowed(match self {
MyNodeTemplate::MakeVector => "New vector",
MyNodeTemplate::MakeScalar => "New scalar", MyNodeTemplate::MakeScalar => "New scalar",
MyNodeTemplate::AddScalar => "Scalar add", MyNodeTemplate::AddScalar => "Scalar add",
MyNodeTemplate::SubtractScalar => "Scalar subtract", MyNodeTemplate::SubtractScalar => "Scalar subtract",
MyNodeTemplate::MakeVector => "New vector",
MyNodeTemplate::AddVector => "Vector add", MyNodeTemplate::AddVector => "Vector add",
MyNodeTemplate::SubtractVector => "Vector subtract", MyNodeTemplate::SubtractVector => "Vector subtract",
MyNodeTemplate::VectorTimesScalar => "Vector times scalar", 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 { 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 // 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. // show different names in the node finder and the node itself.

View File

@ -2,6 +2,8 @@
#![cfg_attr(not(debug_assertions), deny(warnings))] // Forbid warnings in release builds #![cfg_attr(not(debug_assertions), deny(warnings))] // Forbid warnings in release builds
#![warn(clippy::all, rust_2018_idioms)] #![warn(clippy::all, rust_2018_idioms)]
use egui_node_graph_example::NodeGraphExample;
// When compiling natively: // When compiling natively:
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
fn main() { fn main() {
@ -14,10 +16,10 @@ fn main() {
cc.egui_ctx.set_visuals(Visuals::dark()); cc.egui_ctx.set_visuals(Visuals::dark());
#[cfg(feature = "persistence")] #[cfg(feature = "persistence")]
{ {
Box::new(egui_node_graph_example::NodeGraphExample::new(cc)) Box::new(NodeGraphExample::new(cc))
} }
#[cfg(not(feature = "persistence"))] #[cfg(not(feature = "persistence"))]
Box::new(egui_node_graph_example::NodeGraphExample::default()) Box::<NodeGraphExample>::default()
}), }),
) )
.expect("Failed to run native example"); .expect("Failed to run native example");