From 568e56ef4ff2f58c8eedc21fcacbafeaa96933e0 Mon Sep 17 00:00:00 2001 From: Kythyria Tieran Date: Thu, 17 Apr 2025 01:19:53 +0100 Subject: [PATCH 01/18] First draft of factoring out the dropdown boilerplate --- .../document/node_graph/node_properties.rs | 150 ++++++++++++------ node-graph/gcore/src/raster/adjustments.rs | 24 +-- node-graph/gcore/src/vector/misc.rs | 31 ++++ 3 files changed, 144 insertions(+), 61 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 648af38dbd..e00ba0f4ea 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -85,6 +85,102 @@ pub fn start_widgets(document_node: &DocumentNode, node_id: NodeId, index: usize widgets } +trait DropdownSource { + type Value: Sized + Copy + Send + Sync + 'static; + + fn into_index(v: Self::Value) -> Option; + fn into_tagged_value(v: Self::Value) -> TaggedValue; + fn from_tagged_value(tv: Option<&TaggedValue>) -> Option; + fn enumerate() -> impl Iterator)>>; +} + +struct DropdownSourceStatic(std::marker::PhantomData); +impl DropdownSource for DropdownSourceStatic +where + T: graphene_core::vector::misc::DropdownableStatic + 'static, + for<'a> &'a T: TryFrom<&'a TaggedValue>, + TaggedValue: From, +{ + type Value = T; + + fn into_index(v: Self::Value) -> Option { + Some(v.as_u32()) + } + fn into_tagged_value(v: Self::Value) -> TaggedValue { + TaggedValue::from(v) + } + fn from_tagged_value(tv: Option<&TaggedValue>) -> Option { + let v_ref: Option<&Self::Value> = tv.map(|tv| tv.try_into().ok()).flatten(); + v_ref.map(|vr| *vr) + } + fn enumerate() -> impl Iterator)>> { + T::list() + .into_iter() + .map(|cat| cat.into_iter().map(|(item, icon)| (*item, format!("{item:?}"), format!("{item}"), icon.map(String::from)))) + } +} + +fn enum_source() -> DropdownSourceStatic { + DropdownSourceStatic(std::marker::PhantomData) +} + +fn radio_buttons(_list: E, document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { + let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); + + let Some(input) = document_node.inputs.get(index) else { + log::warn!("A widget failed to be built because its node's input index is invalid."); + return LayoutGroup::Row { widgets: vec![] }; + }; + + if let Some(current) = E::from_tagged_value(input.as_non_exposed_value()) { + let items = E::enumerate() + .flatten() + .map(|(item, valstr, label, icon)| { + let red = RadioEntryData::new(valstr) + .on_update(update_value(move |_| E::into_tagged_value(item), node_id, index)) + .on_commit(commit_value); + let red = if let Some(icon) = icon { red.icon(icon).tooltip(label) } else { red.label(label) }; + red + }) + .collect(); + widgets.extend_from_slice(&[ + Separator::new(SeparatorType::Unrelated).widget_holder(), + RadioInput::new(items).selected_index(E::into_index(current)).widget_holder() + ]); + } + LayoutGroup::Row { widgets } +} + +fn dropdown(_list: E, document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { + let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); + + let Some(input) = document_node.inputs.get(index) else { + log::warn!("A widget failed to be built because its node's input index is invalid."); + return LayoutGroup::Row { widgets: vec![] }; + }; + + if let Some(current) = E::from_tagged_value(input.as_non_exposed_value()) { + let items = E::enumerate() + .map(|category| { + category + .map(|(item, valstr, label, _)| { + MenuListEntry::new(valstr) + .label(label) + .on_update(update_value(move |_| E::into_tagged_value(item), node_id, index)) + .on_commit(commit_value) + }) + .collect() + }) + .collect(); + + widgets.extend_from_slice(&[ + Separator::new(SeparatorType::Unrelated).widget_holder(), + DropdownInput::new(items).selected_index(E::into_index(current)).widget_holder() + ]); + } + LayoutGroup::Row { widgets } +} + pub(crate) fn property_from_type( node_id: NodeId, index: usize, @@ -1006,62 +1102,12 @@ pub fn blend_mode(document_node: &DocumentNode, node_id: NodeId, index: usize, n LayoutGroup::Row { widgets }.with_tooltip("Formula used for blending") } -// TODO: Generalize this for all dropdowns (also see blend_mode and channel_extration) pub fn luminance_calculation(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { - let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); - let Some(input) = document_node.inputs.get(index) else { - log::warn!("A widget failed to be built because its node's input index is invalid."); - return LayoutGroup::Row { widgets: vec![] }; - }; - if let Some(&TaggedValue::LuminanceCalculation(calculation)) = input.as_non_exposed_value() { - let calculation_modes = LuminanceCalculation::list(); - let mut entries = Vec::with_capacity(calculation_modes.len()); - for method in calculation_modes { - entries.push( - MenuListEntry::new(format!("{method:?}")) - .label(method.to_string()) - .on_update(update_value(move |_| TaggedValue::LuminanceCalculation(method), node_id, index)) - .on_commit(commit_value), - ); - } - let entries = vec![entries]; - - widgets.extend_from_slice(&[ - Separator::new(SeparatorType::Unrelated).widget_holder(), - DropdownInput::new(entries).selected_index(Some(calculation as u32)).widget_holder(), - ]); - } - LayoutGroup::Row { widgets }.with_tooltip("Formula used to calculate the luminance of a pixel") + dropdown(enum_source::(), document_node, node_id, index, name, blank_assist) } pub fn boolean_operation_radio_buttons(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { - let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); - - let Some(input) = document_node.inputs.get(index) else { - log::warn!("A widget failed to be built because its node's input index is invalid."); - return LayoutGroup::Row { widgets: vec![] }; - }; - if let Some(&TaggedValue::BooleanOperation(calculation)) = input.as_non_exposed_value() { - let operations = BooleanOperation::list(); - let icons = BooleanOperation::icons(); - let mut entries = Vec::with_capacity(operations.len()); - - for (operation, icon) in operations.into_iter().zip(icons.into_iter()) { - entries.push( - RadioEntryData::new(format!("{operation:?}")) - .icon(icon) - .tooltip(operation.to_string()) - .on_update(update_value(move |_| TaggedValue::BooleanOperation(operation), node_id, index)) - .on_commit(commit_value), - ); - } - - widgets.extend_from_slice(&[ - Separator::new(SeparatorType::Unrelated).widget_holder(), - RadioInput::new(entries).selected_index(Some(calculation as u32)).widget_holder(), - ]); - } - LayoutGroup::Row { widgets } + radio_buttons(enum_source::(), document_node, node_id, index, name, blank_assist) } pub fn line_cap_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index bd35b3707f..48b432eb06 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -44,15 +44,21 @@ pub enum LuminanceCalculation { MaximumChannels, } -impl LuminanceCalculation { - pub fn list() -> [LuminanceCalculation; 5] { - [ - LuminanceCalculation::SRGB, - LuminanceCalculation::Perceptual, - LuminanceCalculation::AverageChannels, - LuminanceCalculation::MinimumChannels, - LuminanceCalculation::MaximumChannels, - ] +impl crate::vector::misc::AsU32 for LuminanceCalculation { + fn as_u32(&self) -> u32 { + *self as u32 + } +} + +impl crate::vector::misc::DropdownableStatic for LuminanceCalculation { + fn list() -> &'static [&'static [(Self, Option<&'static str>)]] { + &[&[ + (LuminanceCalculation::SRGB, None), + (LuminanceCalculation::Perceptual, None), + (LuminanceCalculation::AverageChannels, None), + (LuminanceCalculation::MinimumChannels, None), + (LuminanceCalculation::MaximumChannels, None), + ]] } } diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index 476963b4bc..0ece7cfcfe 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -36,6 +36,24 @@ impl BooleanOperation { } } +impl AsU32 for BooleanOperation { + fn as_u32(&self) -> u32 { + *self as u32 + } +} + +impl DropdownableStatic for BooleanOperation { + fn list() -> &'static [&'static [(Self, Option<&'static str>)]] { + &[&[ + (BooleanOperation::Union, Some("BooleanUnion")), + (BooleanOperation::SubtractFront, Some("BooleanSubtractFront")), + (BooleanOperation::SubtractBack, Some("BooleanSubtractBack")), + (BooleanOperation::Intersect, Some("BooleanIntersect")), + (BooleanOperation::Difference, Some("BooleanDifference")), + ]] + } +} + impl core::fmt::Display for BooleanOperation { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { @@ -48,6 +66,19 @@ impl core::fmt::Display for BooleanOperation { } } +pub trait DropdownableStatic: Sized + Copy + AsU32 + std::fmt::Display + std::fmt::Debug + Send + Sync { + fn list() -> &'static [&'static [(Self, Option<&'static str>)]]; +} + +pub trait AsU32 { + fn as_u32(&self) -> u32; +} +impl AsU32 for u32 { + fn as_u32(&self) -> u32 { + *self as u32 + } +} + pub trait AsU64 { fn as_u64(&self) -> u64; } From 389ef10c678aaca10670f69282d90acb28f8563d Mon Sep 17 00:00:00 2001 From: Kythyria Tieran Date: Fri, 18 Apr 2025 09:41:18 +0100 Subject: [PATCH 02/18] Add proc macro for enum boilerplate --- Cargo.lock | 1 + .../menu_bar/menu_bar_message_handler.rs | 13 +- .../tool/tool_messages/select_tool.rs | 9 +- node-graph/gcore/src/raster/adjustments.rs | 32 +--- node-graph/gcore/src/vector/misc.rs | 57 ++----- .../node-macro/src/derive_graphene_rna.rs | 156 ++++++++++++++++++ node-graph/node-macro/src/lib.rs | 13 ++ proc-macros/Cargo.toml | 1 + 8 files changed, 192 insertions(+), 90 deletions(-) create mode 100644 node-graph/node-macro/src/derive_graphene_rna.rs diff --git a/Cargo.lock b/Cargo.lock index 5afed9afa4..42c2c4865e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2403,6 +2403,7 @@ dependencies = [ name = "graphite-proc-macros" version = "0.0.0" dependencies = [ + "convert_case 0.7.1", "graphite-editor", "proc-macro2", "quote", diff --git a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs index cabb466374..5ee78b77f4 100644 --- a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs +++ b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs @@ -414,16 +414,15 @@ impl LayoutHolder for MenuBarMessageHandler { action: MenuBarEntry::no_action(), disabled: no_active_document || !has_selected_layers, children: MenuBarEntryChildren(vec![{ - let operations = BooleanOperation::list(); - let icons = BooleanOperation::icons(); - operations - .into_iter() - .zip(icons) + let list = ::list(); + list.into_iter() + .map(|i| i.into_iter()) + .flatten() .map(move |(operation, icon)| MenuBarEntry { label: operation.to_string(), - icon: Some(icon.into()), + icon: Some(icon.unwrap().into()), action: MenuBarEntry::create_action(move |_| { - let group_folder_type = GroupFolderType::BooleanOperation(operation); + let group_folder_type = GroupFolderType::BooleanOperation(*operation); DocumentMessage::GroupSelectedLayers { group_folder_type }.into() }), disabled: no_active_document || !has_selected_layers, diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index fc897a9a7a..46dc878b51 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -180,14 +180,13 @@ impl SelectTool { } fn boolean_widgets(&self, selected_count: usize) -> impl Iterator + use<> { - let operations = BooleanOperation::list(); - let icons = BooleanOperation::icons(); - operations.into_iter().zip(icons).map(move |(operation, icon)| { - IconButton::new(icon, 24) + let list = ::list(); + list.into_iter().map(|i| i.into_iter()).flatten().map(move |(operation, icon)| { + IconButton::new(icon.unwrap(), 24) .tooltip(operation.to_string()) .disabled(selected_count == 0) .on_update(move |_| { - let group_folder_type = GroupFolderType::BooleanOperation(operation); + let group_folder_type = GroupFolderType::BooleanOperation(*operation); DocumentMessage::GroupSelectedLayers { group_folder_type }.into() }) .widget_holder() diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index 48b432eb06..36d472ec6a 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -34,7 +34,7 @@ use spirv_std::num_traits::float::Float; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] -#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash)] +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash, node_macro::GrapheneRna)] pub enum LuminanceCalculation { #[default] SRGB, @@ -44,36 +44,6 @@ pub enum LuminanceCalculation { MaximumChannels, } -impl crate::vector::misc::AsU32 for LuminanceCalculation { - fn as_u32(&self) -> u32 { - *self as u32 - } -} - -impl crate::vector::misc::DropdownableStatic for LuminanceCalculation { - fn list() -> &'static [&'static [(Self, Option<&'static str>)]] { - &[&[ - (LuminanceCalculation::SRGB, None), - (LuminanceCalculation::Perceptual, None), - (LuminanceCalculation::AverageChannels, None), - (LuminanceCalculation::MinimumChannels, None), - (LuminanceCalculation::MaximumChannels, None), - ]] - } -} - -impl core::fmt::Display for LuminanceCalculation { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - LuminanceCalculation::SRGB => write!(f, "sRGB"), - LuminanceCalculation::Perceptual => write!(f, "Perceptual"), - LuminanceCalculation::AverageChannels => write!(f, "Average Channels"), - LuminanceCalculation::MinimumChannels => write!(f, "Minimum Channels"), - LuminanceCalculation::MaximumChannels => write!(f, "Maximum Channels"), - } - } -} - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] #[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash)] diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index 0ece7cfcfe..58298e90d4 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -10,60 +10,23 @@ pub enum CentroidType { Length, } -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::GrapheneRna)] pub enum BooleanOperation { #[default] + #[rna(icon("BooleanUnion"))] Union, - SubtractFront, - SubtractBack, - Intersect, - Difference, -} -impl BooleanOperation { - pub fn list() -> [BooleanOperation; 5] { - [ - BooleanOperation::Union, - BooleanOperation::SubtractFront, - BooleanOperation::SubtractBack, - BooleanOperation::Intersect, - BooleanOperation::Difference, - ] - } - - pub fn icons() -> [&'static str; 5] { - ["BooleanUnion", "BooleanSubtractFront", "BooleanSubtractBack", "BooleanIntersect", "BooleanDifference"] - } -} + #[rna(icon("BooleanSubtractFront"))] + SubtractFront, -impl AsU32 for BooleanOperation { - fn as_u32(&self) -> u32 { - *self as u32 - } -} + #[rna(icon("BooleanSubtractBack"))] + SubtractBack, -impl DropdownableStatic for BooleanOperation { - fn list() -> &'static [&'static [(Self, Option<&'static str>)]] { - &[&[ - (BooleanOperation::Union, Some("BooleanUnion")), - (BooleanOperation::SubtractFront, Some("BooleanSubtractFront")), - (BooleanOperation::SubtractBack, Some("BooleanSubtractBack")), - (BooleanOperation::Intersect, Some("BooleanIntersect")), - (BooleanOperation::Difference, Some("BooleanDifference")), - ]] - } -} + #[rna(icon("BooleanIntersect"))] + Intersect, -impl core::fmt::Display for BooleanOperation { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - BooleanOperation::Union => write!(f, "Union"), - BooleanOperation::SubtractFront => write!(f, "Subtract Front"), - BooleanOperation::SubtractBack => write!(f, "Subtract Back"), - BooleanOperation::Intersect => write!(f, "Intersect"), - BooleanOperation::Difference => write!(f, "Difference"), - } - } + #[rna(icon("BooleanDifference"))] + Difference, } pub trait DropdownableStatic: Sized + Copy + AsU32 + std::fmt::Display + std::fmt::Debug + Send + Sync { diff --git a/node-graph/node-macro/src/derive_graphene_rna.rs b/node-graph/node-macro/src/derive_graphene_rna.rs new file mode 100644 index 0000000000..36effe9cea --- /dev/null +++ b/node-graph/node-macro/src/derive_graphene_rna.rs @@ -0,0 +1,156 @@ +use proc_macro2::{Ident, TokenStream}; +use quote::quote; +use syn::spanned::Spanned; +use syn::{Attribute, DeriveInput, LitStr, MetaList, Token}; + +pub fn derive_graphene_rna_impl(input_item: TokenStream) -> syn::Result { + let input = syn::parse2::(input_item).unwrap(); + + match input.data { + syn::Data::Enum(en) => derive_enum(input.ident, en), + _ => Err(syn::Error::new(input.ident.span(), "Only enums are supported at the moment")), + } +} + +#[derive(Default)] +struct BasicRna { + label: String, + description: Option, + icon: Option, +} +impl BasicRna { + fn from_attribute(attr: &Attribute) -> syn::Result> { + if !attr.path().is_ident("rna") { + return Ok(None); + } + + attr.parse_args_with(Self::parse_meta) + } + + fn parse_meta(buf: &syn::parse::ParseBuffer<'_>) -> syn::Result> { + let mut res = BasicRna::default(); + + if buf.peek(LitStr) { + let label_tok: LitStr = buf.parse()?; + res.label = label_tok.value(); + + if buf.is_empty() { + return Ok(Some(res)); + } else { + let _ = buf.parse::()?; + } + } + + while !buf.is_empty() { + let item: MetaList = buf.parse()?; + if item.path.is_ident("doc") { + let doc_tok: LitStr = item.parse_args()?; + res.description = Some(doc_tok.value()); + } else if item.path.is_ident("icon") { + let icon_tok: LitStr = item.parse_args()?; + res.icon = Some(icon_tok.value()); + } else { + return Err(syn::Error::new(item.path.span(), "Unexpected meta item")); + } + + if buf.is_empty() { + break; + } + + let _ = buf.parse::()?; + } + Ok(Some(res)) + } + + fn merge(&mut self, rhs: BasicRna) { + if rhs.label.len() > 0 { + self.label = rhs.label; + } + if let Some(d) = rhs.description { + self.description = Some(d) + }; + if let Some(i) = rhs.icon { + self.icon = Some(i) + }; + } +} + +struct Variant { + name: Ident, + basic_rna: BasicRna, +} + +fn derive_enum(name: Ident, input: syn::DataEnum) -> syn::Result { + let mut variants = vec![Vec::new()]; + for va in &input.variants { + if va.attrs.iter().any(|a| a.path().is_ident("menu_separator")) { + variants.push(Vec::new()); + } + + let mut basic_rna = BasicRna::default(); + for attr in &va.attrs { + if let Some(ra) = BasicRna::from_attribute(attr)? { + basic_rna.merge(ra); + } + } + if basic_rna.label.len() == 0 { + basic_rna.label = ident_to_label(&va.ident); + } + + variants.last_mut().unwrap().push(Variant { name: va.ident.clone(), basic_rna }) + } + + let group: Vec<_> = variants + .iter() + .map(|vg| { + let items = vg + .iter() + .map(|v| { + let vname = &v.name; + let icon = match &v.basic_rna.icon { + Some(s) => quote! { Some(#s) }, + None => quote! { None }, + }; + quote! { ( #name::#vname, #icon), } + }) + .collect::>(); + quote! { &[ #(#items)* ], } + }) + .collect(); + let display_arm: Vec<_> = variants + .iter() + .map(|vg| vg.iter()) + .flatten() + .map(|v| { + let vn = &v.name; + let vl = &v.basic_rna.label; + quote! { #name::#vn => write!(f, #vl), } + }) + .collect(); + Ok(quote! { + impl crate::vector::misc::AsU32 for #name { + fn as_u32(&self) -> u32 { + *self as u32 + } + } + + impl crate::vector::misc::DropdownableStatic for #name { + fn list() -> &'static [&'static [(Self, Option<&'static str>)]] { + &[ #(#group)* ] + } + } + + impl core::fmt::Display for #name { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + #( #display_arm )* + } + } + } + }) +} + +fn ident_to_label(id: &Ident) -> String { + use convert_case::{Case, Casing}; + id.to_string().from_case(Case::Pascal).to_case(Case::Title) +} diff --git a/node-graph/node-macro/src/lib.rs b/node-graph/node-macro/src/lib.rs index 67f6fc985a..ea76d9c51d 100644 --- a/node-graph/node-macro/src/lib.rs +++ b/node-graph/node-macro/src/lib.rs @@ -10,9 +10,22 @@ use syn::{ }; mod codegen; +mod derive_graphene_rna; mod parsing; mod validation; +/// Generate meta-information for a type +/// +/// Currently this only works for enums, and derives [`core::fmt::Display`]. +/// +/// `#[rna("Foo")]` on a variant overrides the default UI label (which is otherwise the name converted to title case). +/// `#[rna(icon("tag"))]` sets the icon to use when a variant is shown in a menu or radio button. +/// `#[rna(doc("blah blah"))]` sets help text for tooltips. +#[proc_macro_derive(GrapheneRna, attributes(rna, menu_separator))] +pub fn derive_graphene_rna(input_item: TokenStream) -> TokenStream { + TokenStream::from(derive_graphene_rna::derive_graphene_rna_impl(input_item.into()).unwrap_or_else(|err| err.to_compile_error())) +} + /// A macro used to construct a proto node implementation from the given struct and the decorated function. /// /// This works by generating two `impl` blocks for the given struct: diff --git a/proc-macros/Cargo.toml b/proc-macros/Cargo.toml index 084226a0a1..923c53b34e 100644 --- a/proc-macros/Cargo.toml +++ b/proc-macros/Cargo.toml @@ -23,6 +23,7 @@ serde-discriminant = [] proc-macro2 = { workspace = true } syn = { workspace = true } quote = { workspace = true } +convert_case = { workspace = true } [dev-dependencies] # Local dependencies From 5f97a772eeb4ab2be64e9d766a07caa1b7a04b64 Mon Sep 17 00:00:00 2001 From: Kythyria Tieran Date: Fri, 18 Apr 2025 10:33:57 +0100 Subject: [PATCH 03/18] Detect whether to say `crate` or the name --- .../node-macro/src/derive_graphene_rna.rs | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/node-graph/node-macro/src/derive_graphene_rna.rs b/node-graph/node-macro/src/derive_graphene_rna.rs index 36effe9cea..22c070d37d 100644 --- a/node-graph/node-macro/src/derive_graphene_rna.rs +++ b/node-graph/node-macro/src/derive_graphene_rna.rs @@ -1,4 +1,4 @@ -use proc_macro2::{Ident, TokenStream}; +use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; use syn::spanned::Spanned; use syn::{Attribute, DeriveInput, LitStr, MetaList, Token}; @@ -100,6 +100,20 @@ fn derive_enum(name: Ident, input: syn::DataEnum) -> syn::Result { variants.last_mut().unwrap().push(Variant { name: va.ident.clone(), basic_rna }) } + let crate_name = proc_macro_crate::crate_name("graphene-core").map_err(|e| { + syn::Error::new( + proc_macro2::Span::call_site(), + format!("Failed to find location of graphene_core. Make sure it is imported as a dependency: {}", e), + ) + })?; + let crate_name = match crate_name { + proc_macro_crate::FoundCrate::Itself => quote!(crate), + proc_macro_crate::FoundCrate::Name(n) => { + let i = Ident::new(&n, Span::call_site()); + quote! {#i} + } + }; + let group: Vec<_> = variants .iter() .map(|vg| { @@ -128,13 +142,13 @@ fn derive_enum(name: Ident, input: syn::DataEnum) -> syn::Result { }) .collect(); Ok(quote! { - impl crate::vector::misc::AsU32 for #name { + impl #crate_name::vector::misc::AsU32 for #name { fn as_u32(&self) -> u32 { *self as u32 } } - impl crate::vector::misc::DropdownableStatic for #name { + impl #crate_name::vector::misc::DropdownableStatic for #name { fn list() -> &'static [&'static [(Self, Option<&'static str>)]] { &[ #(#group)* ] } From e8750b012dfca2121c1ddde60838c49cba7e3440 Mon Sep 17 00:00:00 2001 From: Kythyria Tieran Date: Sat, 19 Apr 2025 04:50:35 +0100 Subject: [PATCH 04/18] Clean up the input and naming of the enum macro --- .../document/node_graph/node_properties.rs | 6 +- .../menu_bar/menu_bar_message_handler.rs | 2 +- .../tool/tool_messages/select_tool.rs | 2 +- node-graph/gcore/src/raster/adjustments.rs | 3 +- node-graph/gcore/src/vector/misc.rs | 21 ++- .../node-macro/src/derive_choicetype.rs | 174 ++++++++++++++++++ .../node-macro/src/derive_graphene_rna.rs | 170 ----------------- node-graph/node-macro/src/lib.rs | 18 +- 8 files changed, 204 insertions(+), 192 deletions(-) create mode 100644 node-graph/node-macro/src/derive_choicetype.rs delete mode 100644 node-graph/node-macro/src/derive_graphene_rna.rs diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 0cf1f8f8c2..02d6052b09 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -96,10 +96,10 @@ trait DropdownSource { fn enumerate() -> impl Iterator)>>; } -struct DropdownSourceStatic(std::marker::PhantomData); +struct DropdownSourceStatic(std::marker::PhantomData); impl DropdownSource for DropdownSourceStatic where - T: graphene_core::vector::misc::DropdownableStatic + 'static, + T: graphene_core::vector::misc::ChoiceTypeStatic + 'static, for<'a> &'a T: TryFrom<&'a TaggedValue>, TaggedValue: From, { @@ -122,7 +122,7 @@ where } } -fn enum_source() -> DropdownSourceStatic { +fn enum_source() -> DropdownSourceStatic { DropdownSourceStatic(std::marker::PhantomData) } diff --git a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs index 5ee78b77f4..e379e9c4ea 100644 --- a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs +++ b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs @@ -414,7 +414,7 @@ impl LayoutHolder for MenuBarMessageHandler { action: MenuBarEntry::no_action(), disabled: no_active_document || !has_selected_layers, children: MenuBarEntryChildren(vec![{ - let list = ::list(); + let list = ::list(); list.into_iter() .map(|i| i.into_iter()) .flatten() diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index b5e9637ec0..3923e03806 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -180,7 +180,7 @@ impl SelectTool { } fn boolean_widgets(&self, selected_count: usize) -> impl Iterator + use<> { - let list = ::list(); + let list = ::list(); list.into_iter().map(|i| i.into_iter()).flatten().map(move |(operation, icon)| { IconButton::new(icon.unwrap(), 24) .tooltip(operation.to_string()) diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index 36d472ec6a..41849f59c6 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -34,7 +34,8 @@ use spirv_std::num_traits::float::Float; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] -#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash, node_macro::GrapheneRna)] +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash, node_macro::ChoiceType)] +#[widget(Dropdown)] pub enum LuminanceCalculation { #[default] SRGB, diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index 53a9f90045..36754319fc 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -10,29 +10,36 @@ pub enum CentroidType { Length, } -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::GrapheneRna)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[widget(Radio)] pub enum BooleanOperation { #[default] - #[rna(icon("BooleanUnion"))] + #[icon("BooleanUnion")] Union, - #[rna(icon("BooleanSubtractFront"))] + #[icon("BooleanSubtractFront")] SubtractFront, - #[rna(icon("BooleanSubtractBack"))] + #[icon("BooleanSubtractBack")] SubtractBack, - #[rna(icon("BooleanIntersect"))] + #[icon("BooleanIntersect")] Intersect, - #[rna(icon("BooleanDifference"))] + #[icon("BooleanDifference")] Difference, } -pub trait DropdownableStatic: Sized + Copy + AsU32 + std::fmt::Display + std::fmt::Debug + Send + Sync { +pub trait ChoiceTypeStatic: Sized + Copy + AsU32 + std::fmt::Display + std::fmt::Debug + Send + Sync { + const WIDGET_HINT: ChoiceWidgetHint; fn list() -> &'static [&'static [(Self, Option<&'static str>)]]; } +pub enum ChoiceWidgetHint { + Dropdown, + RadioButtons, +} + pub trait AsU32 { fn as_u32(&self) -> u32; } diff --git a/node-graph/node-macro/src/derive_choicetype.rs b/node-graph/node-macro/src/derive_choicetype.rs new file mode 100644 index 0000000000..4b0028e97b --- /dev/null +++ b/node-graph/node-macro/src/derive_choicetype.rs @@ -0,0 +1,174 @@ +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; +use syn::parse::Parse; +use syn::{Attribute, DeriveInput, Expr, LitStr, Meta}; + +pub fn derive_choicetype_impl(input_item: TokenStream) -> syn::Result { + let input = syn::parse2::(input_item).unwrap(); + + match input.data { + syn::Data::Enum(en) => derive_enum(&input.attrs, input.ident, en), + _ => Err(syn::Error::new(input.ident.span(), "Only enums are supported at the moment")), + } +} + +struct Type { + basic_item: BasicItem, + widget_hint: WidgetHint, +} + +enum WidgetHint { + Radio, + Dropdown, +} +impl Parse for WidgetHint { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let tok: Ident = input.parse()?; + if tok == "Radio" { + Ok(Self::Radio) + } else if tok == "Dropdown" { + Ok(Self::Dropdown) + } else { + Err(syn::Error::new_spanned(tok, "Widget must be either Radio or Dropdown")) + } + } +} + +#[derive(Default)] +struct BasicItem { + label: String, + description: Option, + icon: Option, +} +impl BasicItem { + fn read_attr(&mut self, attr: &Attribute) -> syn::Result<()> { + if attr.path().is_ident("label") { + let tok: LitStr = attr.parse_args()?; + self.label = tok.value(); + } + if attr.path().is_ident("icon") { + let tok: LitStr = attr.parse_args()?; + self.icon = Some(tok.value()); + } + if attr.path().is_ident("doc") { + if let Meta::NameValue(nv) = &attr.meta { + if let Expr::Lit(el) = &nv.value { + if let syn::Lit::Str(tok) = &el.lit { + self.description = Some(tok.value()); + } + } + } + } + Ok(()) + } +} + +struct Variant { + name: Ident, + basic_item: BasicItem, +} + +fn derive_enum(enum_attrs: &[Attribute], name: Ident, input: syn::DataEnum) -> syn::Result { + let mut enum_info = Type { + basic_item: BasicItem::default(), + widget_hint: WidgetHint::Dropdown, + }; + for att in enum_attrs { + enum_info.basic_item.read_attr(att)?; + if att.path().is_ident("widget") { + enum_info.widget_hint = att.parse_args()?; + } + } + + let mut variants = vec![Vec::new()]; + for va in &input.variants { + let mut basic_item = BasicItem::default(); + + for attr in &va.attrs { + if attr.path().is_ident("menu_separator") { + attr.meta.require_path_only()?; + variants.push(Vec::new()); + } + basic_item.read_attr(attr)?; + } + + if basic_item.label.len() == 0 { + basic_item.label = ident_to_label(&va.ident); + } + + variants.last_mut().unwrap().push(Variant { name: va.ident.clone(), basic_item }) + } + + let crate_name = proc_macro_crate::crate_name("graphene-core").map_err(|e| { + syn::Error::new( + proc_macro2::Span::call_site(), + format!("Failed to find location of graphene_core. Make sure it is imported as a dependency: {}", e), + ) + })?; + let crate_name = match crate_name { + proc_macro_crate::FoundCrate::Itself => quote!(crate), + proc_macro_crate::FoundCrate::Name(n) => { + let i = Ident::new(&n, Span::call_site()); + quote! {#i} + } + }; + + let group: Vec<_> = variants + .iter() + .map(|vg| { + let items = vg + .iter() + .map(|v| { + let vname = &v.name; + let icon = match &v.basic_item.icon { + Some(s) => quote! { Some(#s) }, + None => quote! { None }, + }; + quote! { ( #name::#vname, #icon), } + }) + .collect::>(); + quote! { &[ #(#items)* ], } + }) + .collect(); + let display_arm: Vec<_> = variants + .iter() + .map(|vg| vg.iter()) + .flatten() + .map(|v| { + let vn = &v.name; + let vl = &v.basic_item.label; + quote! { #name::#vn => write!(f, #vl), } + }) + .collect(); + let widget_hint = match enum_info.widget_hint { + WidgetHint::Radio => quote! { RadioButtons }, + WidgetHint::Dropdown => quote! { Dropdown }, + }; + Ok(quote! { + impl #crate_name::vector::misc::AsU32 for #name { + fn as_u32(&self) -> u32 { + *self as u32 + } + } + + impl #crate_name::vector::misc::ChoiceTypeStatic for #name { + const WIDGET_HINT: #crate_name::vector::misc::ChoiceWidgetHint = #crate_name::vector::misc::ChoiceWidgetHint::#widget_hint; + fn list() -> &'static [&'static [(Self, Option<&'static str>)]] { + &[ #(#group)* ] + } + } + + impl core::fmt::Display for #name { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + #( #display_arm )* + } + } + } + }) +} + +fn ident_to_label(id: &Ident) -> String { + use convert_case::{Case, Casing}; + id.to_string().from_case(Case::Pascal).to_case(Case::Title) +} diff --git a/node-graph/node-macro/src/derive_graphene_rna.rs b/node-graph/node-macro/src/derive_graphene_rna.rs deleted file mode 100644 index 22c070d37d..0000000000 --- a/node-graph/node-macro/src/derive_graphene_rna.rs +++ /dev/null @@ -1,170 +0,0 @@ -use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; -use syn::spanned::Spanned; -use syn::{Attribute, DeriveInput, LitStr, MetaList, Token}; - -pub fn derive_graphene_rna_impl(input_item: TokenStream) -> syn::Result { - let input = syn::parse2::(input_item).unwrap(); - - match input.data { - syn::Data::Enum(en) => derive_enum(input.ident, en), - _ => Err(syn::Error::new(input.ident.span(), "Only enums are supported at the moment")), - } -} - -#[derive(Default)] -struct BasicRna { - label: String, - description: Option, - icon: Option, -} -impl BasicRna { - fn from_attribute(attr: &Attribute) -> syn::Result> { - if !attr.path().is_ident("rna") { - return Ok(None); - } - - attr.parse_args_with(Self::parse_meta) - } - - fn parse_meta(buf: &syn::parse::ParseBuffer<'_>) -> syn::Result> { - let mut res = BasicRna::default(); - - if buf.peek(LitStr) { - let label_tok: LitStr = buf.parse()?; - res.label = label_tok.value(); - - if buf.is_empty() { - return Ok(Some(res)); - } else { - let _ = buf.parse::()?; - } - } - - while !buf.is_empty() { - let item: MetaList = buf.parse()?; - if item.path.is_ident("doc") { - let doc_tok: LitStr = item.parse_args()?; - res.description = Some(doc_tok.value()); - } else if item.path.is_ident("icon") { - let icon_tok: LitStr = item.parse_args()?; - res.icon = Some(icon_tok.value()); - } else { - return Err(syn::Error::new(item.path.span(), "Unexpected meta item")); - } - - if buf.is_empty() { - break; - } - - let _ = buf.parse::()?; - } - Ok(Some(res)) - } - - fn merge(&mut self, rhs: BasicRna) { - if rhs.label.len() > 0 { - self.label = rhs.label; - } - if let Some(d) = rhs.description { - self.description = Some(d) - }; - if let Some(i) = rhs.icon { - self.icon = Some(i) - }; - } -} - -struct Variant { - name: Ident, - basic_rna: BasicRna, -} - -fn derive_enum(name: Ident, input: syn::DataEnum) -> syn::Result { - let mut variants = vec![Vec::new()]; - for va in &input.variants { - if va.attrs.iter().any(|a| a.path().is_ident("menu_separator")) { - variants.push(Vec::new()); - } - - let mut basic_rna = BasicRna::default(); - for attr in &va.attrs { - if let Some(ra) = BasicRna::from_attribute(attr)? { - basic_rna.merge(ra); - } - } - if basic_rna.label.len() == 0 { - basic_rna.label = ident_to_label(&va.ident); - } - - variants.last_mut().unwrap().push(Variant { name: va.ident.clone(), basic_rna }) - } - - let crate_name = proc_macro_crate::crate_name("graphene-core").map_err(|e| { - syn::Error::new( - proc_macro2::Span::call_site(), - format!("Failed to find location of graphene_core. Make sure it is imported as a dependency: {}", e), - ) - })?; - let crate_name = match crate_name { - proc_macro_crate::FoundCrate::Itself => quote!(crate), - proc_macro_crate::FoundCrate::Name(n) => { - let i = Ident::new(&n, Span::call_site()); - quote! {#i} - } - }; - - let group: Vec<_> = variants - .iter() - .map(|vg| { - let items = vg - .iter() - .map(|v| { - let vname = &v.name; - let icon = match &v.basic_rna.icon { - Some(s) => quote! { Some(#s) }, - None => quote! { None }, - }; - quote! { ( #name::#vname, #icon), } - }) - .collect::>(); - quote! { &[ #(#items)* ], } - }) - .collect(); - let display_arm: Vec<_> = variants - .iter() - .map(|vg| vg.iter()) - .flatten() - .map(|v| { - let vn = &v.name; - let vl = &v.basic_rna.label; - quote! { #name::#vn => write!(f, #vl), } - }) - .collect(); - Ok(quote! { - impl #crate_name::vector::misc::AsU32 for #name { - fn as_u32(&self) -> u32 { - *self as u32 - } - } - - impl #crate_name::vector::misc::DropdownableStatic for #name { - fn list() -> &'static [&'static [(Self, Option<&'static str>)]] { - &[ #(#group)* ] - } - } - - impl core::fmt::Display for #name { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - #( #display_arm )* - } - } - } - }) -} - -fn ident_to_label(id: &Ident) -> String { - use convert_case::{Case, Casing}; - id.to_string().from_case(Case::Pascal).to_case(Case::Title) -} diff --git a/node-graph/node-macro/src/lib.rs b/node-graph/node-macro/src/lib.rs index ea76d9c51d..2295901a3d 100644 --- a/node-graph/node-macro/src/lib.rs +++ b/node-graph/node-macro/src/lib.rs @@ -10,20 +10,20 @@ use syn::{ }; mod codegen; -mod derive_graphene_rna; +mod derive_choicetype; mod parsing; mod validation; -/// Generate meta-information for a type +/// Generate meta-information for an enum. /// -/// Currently this only works for enums, and derives [`core::fmt::Display`]. +/// `#[widget(F)]` on a type indicates the type of widget to use to display/edit the type, currently `Radio` and `Dropdown` are supported. /// -/// `#[rna("Foo")]` on a variant overrides the default UI label (which is otherwise the name converted to title case). -/// `#[rna(icon("tag"))]` sets the icon to use when a variant is shown in a menu or radio button. -/// `#[rna(doc("blah blah"))]` sets help text for tooltips. -#[proc_macro_derive(GrapheneRna, attributes(rna, menu_separator))] -pub fn derive_graphene_rna(input_item: TokenStream) -> TokenStream { - TokenStream::from(derive_graphene_rna::derive_graphene_rna_impl(input_item.into()).unwrap_or_else(|err| err.to_compile_error())) +/// `#[label("Foo")]` on a variant overrides the default UI label (which is otherwise the name converted to title case). Default or not, labels are collected as the [`core::fmt::Display`] implementation. +/// `#[icon("tag"))]` sets the icon to use when a variant is shown in a menu or radio button. +/// Doc comments on a variant become tooltip text. +#[proc_macro_derive(ChoiceType, attributes(widget, menu_separator, label, icon))] +pub fn derive_choice_type(input_item: TokenStream) -> TokenStream { + TokenStream::from(derive_choicetype::derive_choicetype_impl(input_item.into()).unwrap_or_else(|err| err.to_compile_error())) } /// A macro used to construct a proto node implementation from the given struct and the decorated function. From 99318d537dd02d7abe8b3218cfe91efa4714531e Mon Sep 17 00:00:00 2001 From: Kythyria Tieran Date: Sat, 19 Apr 2025 05:54:23 +0100 Subject: [PATCH 05/18] Rename a file --- .../src/{derive_choicetype.rs => derive_choice_type.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename node-graph/node-macro/src/{derive_choicetype.rs => derive_choice_type.rs} (100%) diff --git a/node-graph/node-macro/src/derive_choicetype.rs b/node-graph/node-macro/src/derive_choice_type.rs similarity index 100% rename from node-graph/node-macro/src/derive_choicetype.rs rename to node-graph/node-macro/src/derive_choice_type.rs From f1c87067b9e83a712c19ce722ec718c8396cc326 Mon Sep 17 00:00:00 2001 From: Kythyria Tieran Date: Sat, 19 Apr 2025 06:04:29 +0100 Subject: [PATCH 06/18] Do the rename of code too --- node-graph/node-macro/src/derive_choice_type.rs | 2 +- node-graph/node-macro/src/lib.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/node-graph/node-macro/src/derive_choice_type.rs b/node-graph/node-macro/src/derive_choice_type.rs index 4b0028e97b..a39a402535 100644 --- a/node-graph/node-macro/src/derive_choice_type.rs +++ b/node-graph/node-macro/src/derive_choice_type.rs @@ -3,7 +3,7 @@ use quote::quote; use syn::parse::Parse; use syn::{Attribute, DeriveInput, Expr, LitStr, Meta}; -pub fn derive_choicetype_impl(input_item: TokenStream) -> syn::Result { +pub fn derive_choice_type_impl(input_item: TokenStream) -> syn::Result { let input = syn::parse2::(input_item).unwrap(); match input.data { diff --git a/node-graph/node-macro/src/lib.rs b/node-graph/node-macro/src/lib.rs index 2295901a3d..706d4196b8 100644 --- a/node-graph/node-macro/src/lib.rs +++ b/node-graph/node-macro/src/lib.rs @@ -10,7 +10,7 @@ use syn::{ }; mod codegen; -mod derive_choicetype; +mod derive_choice_type; mod parsing; mod validation; @@ -23,7 +23,7 @@ mod validation; /// Doc comments on a variant become tooltip text. #[proc_macro_derive(ChoiceType, attributes(widget, menu_separator, label, icon))] pub fn derive_choice_type(input_item: TokenStream) -> TokenStream { - TokenStream::from(derive_choicetype::derive_choicetype_impl(input_item.into()).unwrap_or_else(|err| err.to_compile_error())) + TokenStream::from(derive_choice_type::derive_choice_type_impl(input_item.into()).unwrap_or_else(|err| err.to_compile_error())) } /// A macro used to construct a proto node implementation from the given struct and the decorated function. From eb010ee46f96f3ff18072a4c2c3279d873cd0f30 Mon Sep 17 00:00:00 2001 From: Kythyria Tieran Date: Sat, 19 Apr 2025 07:31:20 +0100 Subject: [PATCH 07/18] Use the attribute-driven selection of radio vs dropdown --- .../document/node_graph/node_properties.rs | 99 +++++++++---------- 1 file changed, 44 insertions(+), 55 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 02d6052b09..308aa5eae9 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -87,17 +87,18 @@ pub fn start_widgets(document_node: &DocumentNode, node_id: NodeId, index: usize widgets } -trait DropdownSource { +trait ChoiceSource { type Value: Sized + Copy + Send + Sync + 'static; fn into_index(v: Self::Value) -> Option; fn into_tagged_value(v: Self::Value) -> TaggedValue; fn from_tagged_value(tv: Option<&TaggedValue>) -> Option; fn enumerate() -> impl Iterator)>>; + fn widget_hint() -> graphene_core::vector::misc::ChoiceWidgetHint; } -struct DropdownSourceStatic(std::marker::PhantomData); -impl DropdownSource for DropdownSourceStatic +struct ChoiceSourceStatic(std::marker::PhantomData); +impl ChoiceSource for ChoiceSourceStatic where T: graphene_core::vector::misc::ChoiceTypeStatic + 'static, for<'a> &'a T: TryFrom<&'a TaggedValue>, @@ -120,13 +121,16 @@ where .into_iter() .map(|cat| cat.into_iter().map(|(item, icon)| (*item, format!("{item:?}"), format!("{item}"), icon.map(String::from)))) } + fn widget_hint() -> graphene_core::vector::misc::ChoiceWidgetHint { + T::WIDGET_HINT + } } -fn enum_source() -> DropdownSourceStatic { - DropdownSourceStatic(std::marker::PhantomData) +fn enum_source() -> ChoiceSourceStatic { + ChoiceSourceStatic(std::marker::PhantomData) } -fn radio_buttons(_list: E, document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, description: &str, blank_assist: bool) -> LayoutGroup { +fn choice_widget(list: E, document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, description: &str, blank_assist: bool) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, description, FrontendGraphDataType::General, blank_assist); let Some(input) = document_node.inputs.get(index) else { @@ -135,52 +139,45 @@ fn radio_buttons(_list: E, document_node: &DocumentNode, node }; if let Some(current) = E::from_tagged_value(input.as_non_exposed_value()) { - let items = E::enumerate() - .flatten() - .map(|(item, valstr, label, icon)| { - let red = RadioEntryData::new(valstr) - .on_update(update_value(move |_| E::into_tagged_value(item), node_id, index)) - .on_commit(commit_value); - let red = if let Some(icon) = icon { red.icon(icon).tooltip(label) } else { red.label(label) }; - red - }) - .collect(); - widgets.extend_from_slice(&[ - Separator::new(SeparatorType::Unrelated).widget_holder(), - RadioInput::new(items).selected_index(E::into_index(current)).widget_holder(), - ]); + let widget = match E::widget_hint() { + graphene_std::vector::misc::ChoiceWidgetHint::Dropdown => dropdown(list, node_id, index, current), + graphene_std::vector::misc::ChoiceWidgetHint::RadioButtons => radio_buttons(list, node_id, index, current), + }; + widgets.extend_from_slice(&[Separator::new(SeparatorType::Unrelated).widget_holder(), widget]); } LayoutGroup::Row { widgets } } -fn dropdown(_list: E, document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, description: &str, blank_assist: bool) -> LayoutGroup { - let mut widgets = start_widgets(document_node, node_id, index, name, description, FrontendGraphDataType::General, blank_assist); +fn radio_buttons(_list: E, node_id: NodeId, index: usize, current: E::Value) -> WidgetHolder { + let items = E::enumerate() + .flatten() + .map(|(item, valstr, label, icon)| { + let red = RadioEntryData::new(valstr) + .on_update(update_value(move |_| E::into_tagged_value(item), node_id, index)) + .on_commit(commit_value); + let red = if let Some(icon) = icon { red.icon(icon).tooltip(label) } else { red.label(label) }; + red + }) + .collect(); - let Some(input) = document_node.inputs.get(index) else { - log::warn!("A widget failed to be built because its node's input index is invalid."); - return LayoutGroup::Row { widgets: vec![] }; - }; + RadioInput::new(items).selected_index(E::into_index(current)).widget_holder() +} - if let Some(current) = E::from_tagged_value(input.as_non_exposed_value()) { - let items = E::enumerate() - .map(|category| { - category - .map(|(item, valstr, label, _)| { - MenuListEntry::new(valstr) - .label(label) - .on_update(update_value(move |_| E::into_tagged_value(item), node_id, index)) - .on_commit(commit_value) - }) - .collect() - }) - .collect(); +fn dropdown(_list: E, node_id: NodeId, index: usize, current: E::Value) -> WidgetHolder { + let items = E::enumerate() + .map(|category| { + category + .map(|(item, valstr, label, _)| { + MenuListEntry::new(valstr) + .label(label) + .on_update(update_value(move |_| E::into_tagged_value(item), node_id, index)) + .on_commit(commit_value) + }) + .collect() + }) + .collect(); - widgets.extend_from_slice(&[ - Separator::new(SeparatorType::Unrelated).widget_holder(), - DropdownInput::new(items).selected_index(E::into_index(current)).widget_holder(), - ]); - } - LayoutGroup::Row { widgets } + DropdownInput::new(items).selected_index(E::into_index(current)).widget_holder() } pub(crate) fn property_from_type( @@ -328,9 +325,9 @@ pub(crate) fn property_from_type( .widget_holder(), ] .into(), - Some(x) if x == TypeId::of::() => boolean_operation_radio_buttons(document_node, node_id, index, name, description, true), + Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), document_node, node_id, index, name, description, true), Some(x) if x == TypeId::of::() => centroid_widget(document_node, node_id, index), - Some(x) if x == TypeId::of::() => luminance_calculation(document_node, node_id, index, name, description, true), + Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), document_node, node_id, index, name, description, true), // Some(x) if x == TypeId::of::() => vec![ // DropdownInput::new( // ImaginateSamplingMethod::list() @@ -1182,14 +1179,6 @@ pub fn blend_mode(document_node: &DocumentNode, node_id: NodeId, index: usize, n LayoutGroup::Row { widgets }.with_tooltip("Formula used for blending") } -pub fn luminance_calculation(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, description: &str, blank_assist: bool) -> LayoutGroup { - dropdown(enum_source::(), document_node, node_id, index, name, description, blank_assist) -} - -pub fn boolean_operation_radio_buttons(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, description: &str, blank_assist: bool) -> LayoutGroup { - radio_buttons(enum_source::(), document_node, node_id, index, name, description, blank_assist) -} - pub fn grid_type_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, description: &str, blank_assist: bool) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, description, FrontendGraphDataType::General, blank_assist); let Some(input) = document_node.inputs.get(index) else { From 0aa0f717a5532cce73fa88bacd0b22a047ffd9e6 Mon Sep 17 00:00:00 2001 From: Kythyria Tieran Date: Sat, 19 Apr 2025 21:14:44 +0100 Subject: [PATCH 08/18] Add a metadata struct and tooltips --- .../document/node_graph/node_properties.rs | 33 +++++++++++-------- .../menu_bar/menu_bar_message_handler.rs | 6 ++-- .../tool/tool_messages/select_tool.rs | 11 +++++-- node-graph/gcore/src/registry.rs | 17 ++++++++++ node-graph/gcore/src/vector/misc.rs | 3 +- .../node-macro/src/derive_choice_type.rs | 20 +++++++++-- 6 files changed, 67 insertions(+), 23 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 308aa5eae9..1d8076907d 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -16,6 +16,7 @@ use graphene_core::raster::{ BlendMode, CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute, SelectiveColorChoice, }; +use graphene_core::registry::VariantMetadata; use graphene_core::text::Font; use graphene_core::vector::misc::CentroidType; use graphene_core::vector::style::{GradientType, LineCap, LineJoin}; @@ -93,7 +94,7 @@ trait ChoiceSource { fn into_index(v: Self::Value) -> Option; fn into_tagged_value(v: Self::Value) -> TaggedValue; fn from_tagged_value(tv: Option<&TaggedValue>) -> Option; - fn enumerate() -> impl Iterator)>>; + fn enumerate() -> impl Iterator>; fn widget_hint() -> graphene_core::vector::misc::ChoiceWidgetHint; } @@ -116,10 +117,8 @@ where let v_ref: Option<&Self::Value> = tv.map(|tv| tv.try_into().ok()).flatten(); v_ref.map(|vr| *vr) } - fn enumerate() -> impl Iterator)>> { - T::list() - .into_iter() - .map(|cat| cat.into_iter().map(|(item, icon)| (*item, format!("{item:?}"), format!("{item}"), icon.map(String::from)))) + fn enumerate() -> impl Iterator> { + T::list().into_iter().map(|i| i.into_iter()) } fn widget_hint() -> graphene_core::vector::misc::ChoiceWidgetHint { T::WIDGET_HINT @@ -150,12 +149,18 @@ fn choice_widget(list: E, document_node: &DocumentNode, node_id fn radio_buttons(_list: E, node_id: NodeId, index: usize, current: E::Value) -> WidgetHolder { let items = E::enumerate() + .into_iter() .flatten() - .map(|(item, valstr, label, icon)| { - let red = RadioEntryData::new(valstr) - .on_update(update_value(move |_| E::into_tagged_value(item), node_id, index)) + .map(|(item, var_meta)| { + let red = RadioEntryData::new(var_meta.name.as_ref()) + .on_update(update_value(move |_| E::into_tagged_value(*item), node_id, index)) .on_commit(commit_value); - let red = if let Some(icon) = icon { red.icon(icon).tooltip(label) } else { red.label(label) }; + let red = match (var_meta.icon.as_deref(), var_meta.docstring.as_deref()) { + (None, None) => red.label(var_meta.label.as_ref()), + (None, Some(doc)) => red.label(var_meta.label.as_ref()).tooltip(doc), + (Some(icon), None) => red.icon(icon).tooltip(var_meta.label.as_ref()), + (Some(icon), Some(doc)) => red.icon(icon).tooltip(format!("{}\n\n{}", var_meta.label, doc)), + }; red }) .collect(); @@ -165,12 +170,14 @@ fn radio_buttons(_list: E, node_id: NodeId, index: usize, curre fn dropdown(_list: E, node_id: NodeId, index: usize, current: E::Value) -> WidgetHolder { let items = E::enumerate() + .into_iter() .map(|category| { category - .map(|(item, valstr, label, _)| { - MenuListEntry::new(valstr) - .label(label) - .on_update(update_value(move |_| E::into_tagged_value(item), node_id, index)) + .into_iter() + .map(|(item, var_meta)| { + MenuListEntry::new(var_meta.name.as_ref()) + .label(var_meta.label.as_ref()) + .on_update(update_value(move |_| E::into_tagged_value(*item), node_id, index)) .on_commit(commit_value) }) .collect() diff --git a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs index e379e9c4ea..ea64387feb 100644 --- a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs +++ b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs @@ -418,9 +418,9 @@ impl LayoutHolder for MenuBarMessageHandler { list.into_iter() .map(|i| i.into_iter()) .flatten() - .map(move |(operation, icon)| MenuBarEntry { - label: operation.to_string(), - icon: Some(icon.unwrap().into()), + .map(move |(operation, info)| MenuBarEntry { + label: info.label.to_string(), + icon: info.icon.as_ref().map(|i| i.to_string()), action: MenuBarEntry::create_action(move |_| { let group_folder_type = GroupFolderType::BooleanOperation(*operation); DocumentMessage::GroupSelectedLayers { group_folder_type }.into() diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 3923e03806..919cc37e28 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -181,9 +181,14 @@ impl SelectTool { fn boolean_widgets(&self, selected_count: usize) -> impl Iterator + use<> { let list = ::list(); - list.into_iter().map(|i| i.into_iter()).flatten().map(move |(operation, icon)| { - IconButton::new(icon.unwrap(), 24) - .tooltip(operation.to_string()) + list.into_iter().map(|i| i.into_iter()).flatten().map(move |(operation, info)| { + let mut tooltip = info.label.to_string(); + if let Some(doc) = info.docstring.as_deref() { + tooltip.push_str("\n\n"); + tooltip.push_str(doc); + } + IconButton::new(info.icon.as_deref().unwrap(), 24) + .tooltip(tooltip) .disabled(selected_count == 0) .on_update(move |_| { let group_folder_type = GroupFolderType::BooleanOperation(*operation); diff --git a/node-graph/gcore/src/registry.rs b/node-graph/gcore/src/registry.rs index 86a51918da..fa40bd6a34 100644 --- a/node-graph/gcore/src/registry.rs +++ b/node-graph/gcore/src/registry.rs @@ -1,5 +1,6 @@ use crate::{Node, NodeIO, NodeIOTypes, Type, WasmNotSend}; use dyn_any::{DynAny, StaticType}; +use std::borrow::Cow; use std::collections::HashMap; use std::marker::PhantomData; use std::ops::Deref; @@ -51,6 +52,22 @@ pub struct FieldMetadata { pub number_mode_range: Option<(f64, f64)>, } +// Translation struct between macro and definition +#[derive(Clone, Debug)] +pub struct VariantMetadata { + /// Name as declared in source code + pub name: Cow<'static, str>, + + /// Name to be displayed in UI + pub label: Cow<'static, str>, + + /// User-facing documentation text + pub docstring: Option>, + + /// Name of icon to display in radio buttons and such + pub icon: Option>, +} + #[derive(Clone, Debug)] pub enum RegistryWidgetOverride { None, diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index 36754319fc..85ce5f4f93 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -20,6 +20,7 @@ pub enum BooleanOperation { #[icon("BooleanSubtractFront")] SubtractFront, + /// Output shape contains points which are contained by the front shape but *not* by the back shape. #[icon("BooleanSubtractBack")] SubtractBack, @@ -32,7 +33,7 @@ pub enum BooleanOperation { pub trait ChoiceTypeStatic: Sized + Copy + AsU32 + std::fmt::Display + std::fmt::Debug + Send + Sync { const WIDGET_HINT: ChoiceWidgetHint; - fn list() -> &'static [&'static [(Self, Option<&'static str>)]]; + fn list() -> &'static [&'static [(Self, crate::registry::VariantMetadata)]]; } pub enum ChoiceWidgetHint { diff --git a/node-graph/node-macro/src/derive_choice_type.rs b/node-graph/node-macro/src/derive_choice_type.rs index a39a402535..36038aea71 100644 --- a/node-graph/node-macro/src/derive_choice_type.rs +++ b/node-graph/node-macro/src/derive_choice_type.rs @@ -120,11 +120,25 @@ fn derive_enum(enum_attrs: &[Attribute], name: Ident, input: syn::DataEnum) -> s .iter() .map(|v| { let vname = &v.name; + let vname_str = v.name.to_string(); + let label = &v.basic_item.label; + let docstring = match &v.basic_item.description { + Some(s) => { + let s = s.trim(); + quote! { Some(::alloc::borrow::Cow::Borrowed(#s)) } + } + None => quote! { None }, + }; let icon = match &v.basic_item.icon { - Some(s) => quote! { Some(#s) }, + Some(s) => quote! { Some(::alloc::borrow::Cow::Borrowed(#s)) }, None => quote! { None }, }; - quote! { ( #name::#vname, #icon), } + quote! { ( #name::#vname, #crate_name::registry::VariantMetadata { + name: ::alloc::borrow::Cow::Borrowed(#vname_str), + label: ::alloc::borrow::Cow::Borrowed(#label), + docstring: #docstring, + icon: #icon, + }), } }) .collect::>(); quote! { &[ #(#items)* ], } @@ -153,7 +167,7 @@ fn derive_enum(enum_attrs: &[Attribute], name: Ident, input: syn::DataEnum) -> s impl #crate_name::vector::misc::ChoiceTypeStatic for #name { const WIDGET_HINT: #crate_name::vector::misc::ChoiceWidgetHint = #crate_name::vector::misc::ChoiceWidgetHint::#widget_hint; - fn list() -> &'static [&'static [(Self, Option<&'static str>)]] { + fn list() -> &'static [&'static [(Self, #crate_name::registry::VariantMetadata)]] { &[ #(#group)* ] } } From 672e6c86cb51440f66b9940db6e20297fbb38081 Mon Sep 17 00:00:00 2001 From: Kythyria Tieran Date: Sun, 20 Apr 2025 03:49:27 +0100 Subject: [PATCH 09/18] Move the new traits to a better place. --- .../document/node_graph/node_properties.rs | 16 ++++++++-------- .../menu_bar/menu_bar_message_handler.rs | 2 +- .../messages/tool/tool_messages/select_tool.rs | 2 +- node-graph/gcore/src/registry.rs | 10 ++++++++++ node-graph/gcore/src/vector/misc.rs | 10 ---------- node-graph/node-macro/src/derive_choice_type.rs | 4 ++-- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 1d8076907d..f0cf12726c 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -16,7 +16,7 @@ use graphene_core::raster::{ BlendMode, CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute, SelectiveColorChoice, }; -use graphene_core::registry::VariantMetadata; +use graphene_core::registry::{ChoiceTypeStatic, ChoiceWidgetHint, VariantMetadata}; use graphene_core::text::Font; use graphene_core::vector::misc::CentroidType; use graphene_core::vector::style::{GradientType, LineCap, LineJoin}; @@ -95,13 +95,13 @@ trait ChoiceSource { fn into_tagged_value(v: Self::Value) -> TaggedValue; fn from_tagged_value(tv: Option<&TaggedValue>) -> Option; fn enumerate() -> impl Iterator>; - fn widget_hint() -> graphene_core::vector::misc::ChoiceWidgetHint; + fn widget_hint() -> ChoiceWidgetHint; } -struct ChoiceSourceStatic(std::marker::PhantomData); +struct ChoiceSourceStatic(std::marker::PhantomData); impl ChoiceSource for ChoiceSourceStatic where - T: graphene_core::vector::misc::ChoiceTypeStatic + 'static, + T: ChoiceTypeStatic + 'static, for<'a> &'a T: TryFrom<&'a TaggedValue>, TaggedValue: From, { @@ -120,12 +120,12 @@ where fn enumerate() -> impl Iterator> { T::list().into_iter().map(|i| i.into_iter()) } - fn widget_hint() -> graphene_core::vector::misc::ChoiceWidgetHint { + fn widget_hint() -> ChoiceWidgetHint { T::WIDGET_HINT } } -fn enum_source() -> ChoiceSourceStatic { +fn enum_source() -> ChoiceSourceStatic { ChoiceSourceStatic(std::marker::PhantomData) } @@ -139,8 +139,8 @@ fn choice_widget(list: E, document_node: &DocumentNode, node_id if let Some(current) = E::from_tagged_value(input.as_non_exposed_value()) { let widget = match E::widget_hint() { - graphene_std::vector::misc::ChoiceWidgetHint::Dropdown => dropdown(list, node_id, index, current), - graphene_std::vector::misc::ChoiceWidgetHint::RadioButtons => radio_buttons(list, node_id, index, current), + ChoiceWidgetHint::Dropdown => dropdown(list, node_id, index, current), + ChoiceWidgetHint::RadioButtons => radio_buttons(list, node_id, index, current), }; widgets.extend_from_slice(&[Separator::new(SeparatorType::Unrelated).widget_holder(), widget]); } diff --git a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs index ea64387feb..0c7886f9ab 100644 --- a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs +++ b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs @@ -414,7 +414,7 @@ impl LayoutHolder for MenuBarMessageHandler { action: MenuBarEntry::no_action(), disabled: no_active_document || !has_selected_layers, children: MenuBarEntryChildren(vec![{ - let list = ::list(); + let list = ::list(); list.into_iter() .map(|i| i.into_iter()) .flatten() diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 919cc37e28..d18a34a5f1 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -180,7 +180,7 @@ impl SelectTool { } fn boolean_widgets(&self, selected_count: usize) -> impl Iterator + use<> { - let list = ::list(); + let list = ::list(); list.into_iter().map(|i| i.into_iter()).flatten().map(move |(operation, info)| { let mut tooltip = info.label.to_string(); if let Some(doc) = info.docstring.as_deref() { diff --git a/node-graph/gcore/src/registry.rs b/node-graph/gcore/src/registry.rs index fa40bd6a34..c61ca66dc8 100644 --- a/node-graph/gcore/src/registry.rs +++ b/node-graph/gcore/src/registry.rs @@ -52,6 +52,16 @@ pub struct FieldMetadata { pub number_mode_range: Option<(f64, f64)>, } +pub trait ChoiceTypeStatic: Sized + Copy + crate::vector::misc::AsU32 + std::fmt::Display + std::fmt::Debug + Send + Sync { + const WIDGET_HINT: ChoiceWidgetHint; + fn list() -> &'static [&'static [(Self, VariantMetadata)]]; +} + +pub enum ChoiceWidgetHint { + Dropdown, + RadioButtons, +} + // Translation struct between macro and definition #[derive(Clone, Debug)] pub struct VariantMetadata { diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index 85ce5f4f93..82275c03b7 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -31,16 +31,6 @@ pub enum BooleanOperation { Difference, } -pub trait ChoiceTypeStatic: Sized + Copy + AsU32 + std::fmt::Display + std::fmt::Debug + Send + Sync { - const WIDGET_HINT: ChoiceWidgetHint; - fn list() -> &'static [&'static [(Self, crate::registry::VariantMetadata)]]; -} - -pub enum ChoiceWidgetHint { - Dropdown, - RadioButtons, -} - pub trait AsU32 { fn as_u32(&self) -> u32; } diff --git a/node-graph/node-macro/src/derive_choice_type.rs b/node-graph/node-macro/src/derive_choice_type.rs index 36038aea71..d67e5ebe56 100644 --- a/node-graph/node-macro/src/derive_choice_type.rs +++ b/node-graph/node-macro/src/derive_choice_type.rs @@ -165,8 +165,8 @@ fn derive_enum(enum_attrs: &[Attribute], name: Ident, input: syn::DataEnum) -> s } } - impl #crate_name::vector::misc::ChoiceTypeStatic for #name { - const WIDGET_HINT: #crate_name::vector::misc::ChoiceWidgetHint = #crate_name::vector::misc::ChoiceWidgetHint::#widget_hint; + impl #crate_name::registry::ChoiceTypeStatic for #name { + const WIDGET_HINT: #crate_name::registry::ChoiceWidgetHint = #crate_name::registry::ChoiceWidgetHint::#widget_hint; fn list() -> &'static [&'static [(Self, #crate_name::registry::VariantMetadata)]] { &[ #(#group)* ] } From 2a8a837f6ffedcbd79d8aa428c5c816202de5e1d Mon Sep 17 00:00:00 2001 From: Kythyria Tieran Date: Mon, 21 Apr 2025 21:39:12 +0100 Subject: [PATCH 10/18] Use ChoiceType, part 1 --- .../document/node_graph/node_properties.rs | 226 ++---------------- node-graph/gcore/src/ops.rs | 3 +- node-graph/gcore/src/raster/adjustments.rs | 61 ++--- node-graph/gcore/src/registry.rs | 2 +- node-graph/gcore/src/vector/misc.rs | 9 +- node-graph/gcore/src/vector/style.rs | 6 +- .../node-macro/src/derive_choice_type.rs | 18 -- node-graph/node-macro/src/lib.rs | 2 +- 8 files changed, 47 insertions(+), 280 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 3be99271cc..8ebeea70e9 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -287,14 +287,14 @@ pub(crate) fn property_from_type( Some(x) if x == TypeId::of::() => cellular_return_type_widget(default_info, false), Some(x) if x == TypeId::of::() => domain_warp_type_widget(default_info, false), Some(x) if x == TypeId::of::() => relative_absolute_widget(default_info), - Some(x) if x == TypeId::of::() => grid_type_widget(default_info), - Some(x) if x == TypeId::of::() => line_cap_widget(default_info), - Some(x) if x == TypeId::of::() => line_join_widget(default_info), - Some(x) if x == TypeId::of::() => arc_type_widget(default_info), + Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), + Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), + Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), + Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), Some(x) if x == TypeId::of::() => fill_type_widget(default_info), Some(x) if x == TypeId::of::() => gradient_type_widget(default_info), Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), - Some(x) if x == TypeId::of::() => centroid_type_widget(default_info), + Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), _ => { let mut widgets = start_widgets(default_info, FrontendGraphDataType::General); @@ -937,60 +937,13 @@ pub fn rgba_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup } pub fn xy_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup { - let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; - - let mut widgets = start_widgets(parameter_widgets_info, FrontendGraphDataType::General); - let Some(input) = document_node.inputs.get(index) else { - log::warn!("A widget failed to be built because its node's input index is invalid."); - return LayoutGroup::Row { widgets: vec![] }; - }; - if let Some(&TaggedValue::XY(mode)) = input.as_non_exposed_value() { - let calculation_modes = [XY::X, XY::Y]; - let mut entries = Vec::with_capacity(calculation_modes.len()); - for method in calculation_modes { - entries.push( - MenuListEntry::new(format!("{method:?}")) - .label(method.to_string()) - .on_update(update_value(move |_| TaggedValue::XY(method), node_id, index)) - .on_commit(commit_value), - ); - } - let entries = vec![entries]; - - widgets.extend_from_slice(&[ - Separator::new(SeparatorType::Unrelated).widget_holder(), - DropdownInput::new(entries).selected_index(Some(mode as u32)).widget_holder(), - ]); - } - LayoutGroup::Row { widgets }.with_tooltip("X or Y Component of Vector2") + // TODO: Put tooltips in choice_widget + choice_widget(enum_source::(), parameter_widgets_info).with_tooltip("X or Y Component of Vector2") } -// TODO: Generalize this instead of using a separate function per dropdown menu enum pub fn noise_type_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup { - let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; - - let mut widgets = start_widgets(parameter_widgets_info, FrontendGraphDataType::General); - let Some(input) = document_node.inputs.get(index) else { - log::warn!("A widget failed to be built because its node's input index is invalid."); - return LayoutGroup::Row { widgets: vec![] }; - }; - if let Some(&TaggedValue::NoiseType(noise_type)) = input.as_non_exposed_value() { - let entries = NoiseType::list() - .iter() - .map(|noise_type| { - MenuListEntry::new(format!("{noise_type:?}")) - .label(noise_type.to_string()) - .on_update(update_value(move |_| TaggedValue::NoiseType(*noise_type), node_id, index)) - .on_commit(commit_value) - }) - .collect(); - - widgets.extend_from_slice(&[ - Separator::new(SeparatorType::Unrelated).widget_holder(), - DropdownInput::new(vec![entries]).selected_index(Some(noise_type as u32)).widget_holder(), - ]); - } - LayoutGroup::Row { widgets }.with_tooltip("Style of noise pattern") + // TODO: Put tooltips in choice_widget + choice_widget(enum_source::(), parameter_widgets_info).with_tooltip("Style of noise pattern") } // TODO: Generalize this instead of using a separate function per dropdown menu enum @@ -1092,10 +1045,12 @@ pub fn domain_warp_type_widget(parameter_widgets_info: ParameterWidgetsInfo, dis if let Some(&TaggedValue::DomainWarpType(domain_warp_type)) = input.as_non_exposed_value() { let entries = DomainWarpType::list() .iter() - .map(|domain_warp_type| { - MenuListEntry::new(format!("{domain_warp_type:?}")) - .label(domain_warp_type.to_string()) - .on_update(update_value(move |_| TaggedValue::DomainWarpType(*domain_warp_type), node_id, index)) + .map(|i| i.into_iter()) + .flatten() + .map(|(item, info)| { + MenuListEntry::new(info.name.as_ref()) + .label(info.label.as_ref()) + .on_update(update_value(move |_| TaggedValue::DomainWarpType(*item), node_id, index)) .on_commit(commit_value) }) .collect(); @@ -1161,114 +1116,6 @@ pub fn blend_mode_widget(parameter_widgets_info: ParameterWidgetsInfo) -> Layout LayoutGroup::Row { widgets }.with_tooltip("Formula used for blending") } -pub fn grid_type_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup { - let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; - - let mut widgets = start_widgets(parameter_widgets_info, FrontendGraphDataType::General); - let Some(input) = document_node.inputs.get(index) else { - log::warn!("A widget failed to be built because its node's input index is invalid."); - return LayoutGroup::Row { widgets: vec![] }; - }; - if let Some(&TaggedValue::GridType(grid_type)) = input.as_non_exposed_value() { - let entries = [("Rectangular", GridType::Rectangular), ("Isometric", GridType::Isometric)] - .into_iter() - .map(|(name, val)| { - RadioEntryData::new(format!("{val:?}")) - .label(name) - .on_update(update_value(move |_| TaggedValue::GridType(val), node_id, index)) - .on_commit(commit_value) - }) - .collect(); - - widgets.extend_from_slice(&[ - Separator::new(SeparatorType::Unrelated).widget_holder(), - RadioInput::new(entries).selected_index(Some(grid_type as u32)).widget_holder(), - ]); - } - LayoutGroup::Row { widgets } -} - -pub fn line_cap_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup { - let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; - - let mut widgets = start_widgets(parameter_widgets_info, FrontendGraphDataType::General); - let Some(input) = document_node.inputs.get(index) else { - log::warn!("A widget failed to be built because its node's input index is invalid."); - return LayoutGroup::Row { widgets: vec![] }; - }; - if let Some(&TaggedValue::LineCap(line_cap)) = input.as_non_exposed_value() { - let entries = [("Butt", LineCap::Butt), ("Round", LineCap::Round), ("Square", LineCap::Square)] - .into_iter() - .map(|(name, val)| { - RadioEntryData::new(format!("{val:?}")) - .label(name) - .on_update(update_value(move |_| TaggedValue::LineCap(val), node_id, index)) - .on_commit(commit_value) - }) - .collect(); - - widgets.extend_from_slice(&[ - Separator::new(SeparatorType::Unrelated).widget_holder(), - RadioInput::new(entries).selected_index(Some(line_cap as u32)).widget_holder(), - ]); - } - LayoutGroup::Row { widgets } -} - -pub fn line_join_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup { - let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; - - let mut widgets = start_widgets(parameter_widgets_info, FrontendGraphDataType::General); - let Some(input) = document_node.inputs.get(index) else { - log::warn!("A widget failed to be built because its node's input index is invalid."); - return LayoutGroup::Row { widgets: vec![] }; - }; - if let Some(&TaggedValue::LineJoin(line_join)) = input.as_non_exposed_value() { - let entries = [("Miter", LineJoin::Miter), ("Bevel", LineJoin::Bevel), ("Round", LineJoin::Round)] - .into_iter() - .map(|(name, val)| { - RadioEntryData::new(format!("{val:?}")) - .label(name) - .on_update(update_value(move |_| TaggedValue::LineJoin(val), node_id, index)) - .on_commit(commit_value) - }) - .collect(); - - widgets.extend_from_slice(&[ - Separator::new(SeparatorType::Unrelated).widget_holder(), - RadioInput::new(entries).selected_index(Some(line_join as u32)).widget_holder(), - ]); - } - LayoutGroup::Row { widgets } -} - -pub fn arc_type_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup { - let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; - - let mut widgets = start_widgets(parameter_widgets_info, FrontendGraphDataType::General); - let Some(input) = document_node.inputs.get(index) else { - log::warn!("A widget failed to be built because its node's input index is invalid."); - return LayoutGroup::Row { widgets: vec![] }; - }; - if let Some(&TaggedValue::ArcType(arc_type)) = input.as_non_exposed_value() { - let entries = [("Open", ArcType::Open), ("Closed", ArcType::Closed), ("Pie Slice", ArcType::PieSlice)] - .into_iter() - .map(|(name, val)| { - RadioEntryData::new(format!("{val:?}")) - .label(name) - .on_update(update_value(move |_| TaggedValue::ArcType(val), node_id, index)) - .on_commit(commit_value) - }) - .collect(); - - widgets.extend_from_slice(&[ - Separator::new(SeparatorType::Unrelated).widget_holder(), - RadioInput::new(entries).selected_index(Some(arc_type as u32)).widget_holder(), - ]); - } - LayoutGroup::Row { widgets } -} - pub fn fill_type_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup { let ParameterWidgetsInfo { node_id, index, .. } = parameter_widgets_info; @@ -1378,41 +1225,6 @@ pub fn curve_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup LayoutGroup::Row { widgets } } -pub fn centroid_type_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup { - let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; - - let mut widgets = start_widgets(parameter_widgets_info, FrontendGraphDataType::General); - let Some(input) = document_node.inputs.get(index) else { - log::warn!("A widget failed to be built because its node's input index is invalid."); - return LayoutGroup::Row { widgets: vec![] }; - }; - if let Some(&TaggedValue::CentroidType(centroid_type)) = input.as_non_exposed_value() { - let entries = vec![ - RadioEntryData::new("area") - .label("Area") - .tooltip("Center of mass for the interior area of the shape") - .on_update(update_value(move |_| TaggedValue::CentroidType(CentroidType::Area), node_id, index)) - .on_commit(commit_value), - RadioEntryData::new("length") - .label("Length") - .tooltip("Center of mass for the perimeter arc length of the shape") - .on_update(update_value(move |_| TaggedValue::CentroidType(CentroidType::Length), node_id, index)) - .on_commit(commit_value), - ]; - - widgets.extend_from_slice(&[ - Separator::new(SeparatorType::Unrelated).widget_holder(), - RadioInput::new(entries) - .selected_index(match centroid_type { - CentroidType::Area => Some(0), - CentroidType::Length => Some(1), - }) - .widget_holder(), - ]); - } - LayoutGroup::Row { widgets } -} - pub fn get_document_node<'a>(node_id: NodeId, context: &'a NodePropertiesContext<'a>) -> Result<&'a DocumentNode, String> { let network = context .network_interface @@ -1674,7 +1486,7 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte return Vec::new(); } }; - let grid_type = grid_type_widget(ParameterWidgetsInfo::from_index(document_node, node_id, grid_type_index, true, context)); + let grid_type = choice_widget(enum_source::(), ParameterWidgetsInfo::from_index(document_node, node_id, grid_type_index, true, context)); let mut widgets = vec![grid_type]; @@ -2189,8 +2001,8 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) - ); let number_input = NumberInput::default().unit(" px").disabled(dash_lengths_val.is_empty()); let dash_offset = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, dash_offset_index, true, context), number_input); - let line_cap = line_cap_widget(ParameterWidgetsInfo::from_index(document_node, node_id, line_cap_index, true, context)); - let line_join = line_join_widget(ParameterWidgetsInfo::from_index(document_node, node_id, line_join_index, true, context)); + let line_cap = choice_widget(enum_source::(), ParameterWidgetsInfo::from_index(document_node, node_id, line_cap_index, true, context)); + let line_join = choice_widget(enum_source::(), ParameterWidgetsInfo::from_index(document_node, node_id, line_join_index, true, context)); let line_join_val = match &document_node.inputs[line_join_index].as_value() { Some(TaggedValue::LineJoin(x)) => x, _ => &LineJoin::Miter, @@ -2226,7 +2038,7 @@ pub fn offset_path_properties(node_id: NodeId, context: &mut NodePropertiesConte let number_input = NumberInput::default().unit(" px"); let distance = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, distance_index, true, context), number_input); - let line_join = line_join_widget(ParameterWidgetsInfo::from_index(document_node, node_id, line_join_index, true, context)); + let line_join = choice_widget(enum_source::(), ParameterWidgetsInfo::from_index(document_node, node_id, line_join_index, true, context)); let line_join_val = match &document_node.inputs[line_join_index].as_value() { Some(TaggedValue::LineJoin(x)) => x, _ => &LineJoin::Miter, diff --git a/node-graph/gcore/src/ops.rs b/node-graph/gcore/src/ops.rs index 8d95d7f8e9..a91fb3eb07 100644 --- a/node-graph/gcore/src/ops.rs +++ b/node-graph/gcore/src/ops.rs @@ -503,7 +503,8 @@ fn extract_xy>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[widget(Dropdown)] pub enum XY { #[default] X, diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index 41849f59c6..3d83d4b917 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -865,46 +865,24 @@ impl core::fmt::Display for RedGreenBlueAlpha { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[widget(Radio)] pub enum NoiseType { #[default] Perlin, + + #[label("OpenSimplex2")] OpenSimplex2, + + #[label("OpenSimplex2S")] OpenSimplex2S, + Cellular, ValueCubic, Value, WhiteNoise, } -impl core::fmt::Display for NoiseType { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - NoiseType::Perlin => write!(f, "Perlin"), - NoiseType::OpenSimplex2 => write!(f, "OpenSimplex2"), - NoiseType::OpenSimplex2S => write!(f, "OpenSimplex2S"), - NoiseType::Cellular => write!(f, "Cellular"), - NoiseType::ValueCubic => write!(f, "Value Cubic"), - NoiseType::Value => write!(f, "Value"), - NoiseType::WhiteNoise => write!(f, "White Noise"), - } - } -} - -impl NoiseType { - pub fn list() -> &'static [NoiseType; 7] { - &[ - NoiseType::Perlin, - NoiseType::OpenSimplex2, - NoiseType::OpenSimplex2S, - NoiseType::Cellular, - NoiseType::ValueCubic, - NoiseType::Value, - NoiseType::WhiteNoise, - ] - } -} - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)] @@ -1021,30 +999,19 @@ impl CellularReturnType { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[widget(Radio)] pub enum DomainWarpType { #[default] None, + + #[label("OpenSimplex2")] OpenSimplex2, - OpenSimplex2Reduced, - BasicGrid, -} -impl core::fmt::Display for DomainWarpType { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - DomainWarpType::None => write!(f, "None"), - DomainWarpType::OpenSimplex2 => write!(f, "OpenSimplex2"), - DomainWarpType::OpenSimplex2Reduced => write!(f, "OpenSimplex2 Reduced"), - DomainWarpType::BasicGrid => write!(f, "Basic Grid"), - } - } -} + #[label("OpenSimplex2 Reduced")] + OpenSimplex2Reduced, -impl DomainWarpType { - pub fn list() -> &'static [DomainWarpType; 4] { - &[DomainWarpType::None, DomainWarpType::OpenSimplex2, DomainWarpType::OpenSimplex2Reduced, DomainWarpType::BasicGrid] - } + BasicGrid, } // Aims for interoperable compatibility with: diff --git a/node-graph/gcore/src/registry.rs b/node-graph/gcore/src/registry.rs index ebc3bf2f6f..b1c0c97469 100644 --- a/node-graph/gcore/src/registry.rs +++ b/node-graph/gcore/src/registry.rs @@ -54,7 +54,7 @@ pub struct FieldMetadata { pub number_mode_range: Option<(f64, f64)>, } -pub trait ChoiceTypeStatic: Sized + Copy + crate::vector::misc::AsU32 + std::fmt::Display + std::fmt::Debug + Send + Sync { +pub trait ChoiceTypeStatic: Sized + Copy + crate::vector::misc::AsU32 + Send + Sync { const WIDGET_HINT: ChoiceWidgetHint; fn list() -> &'static [&'static [(Self, VariantMetadata)]]; } diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index 8cd97cf860..41f025299f 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -1,7 +1,8 @@ use dyn_any::DynAny; /// Represents different ways of calculating the centroid. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[widget(Radio)] pub enum CentroidType { /// The center of mass for the area of a solid shape's interior, as if made out of an infinitely flat material. #[default] @@ -78,7 +79,8 @@ impl AsI64 for f64 { } } -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[widget(Radio)] pub enum GridType { #[default] Rectangular, @@ -86,7 +88,8 @@ pub enum GridType { } #[repr(C)] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[widget(Radio)] pub enum ArcType { #[default] Open, diff --git a/node-graph/gcore/src/vector/style.rs b/node-graph/gcore/src/vector/style.rs index 7312fc062d..bc42df21c6 100644 --- a/node-graph/gcore/src/vector/style.rs +++ b/node-graph/gcore/src/vector/style.rs @@ -471,7 +471,8 @@ pub enum FillType { /// The stroke (outline) style of an SVG element. #[repr(C)] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[widget(Radio)] pub enum LineCap { #[default] Butt, @@ -490,7 +491,8 @@ impl Display for LineCap { } #[repr(C)] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[widget(Radio)] pub enum LineJoin { #[default] Miter, diff --git a/node-graph/node-macro/src/derive_choice_type.rs b/node-graph/node-macro/src/derive_choice_type.rs index d67e5ebe56..933c3f041a 100644 --- a/node-graph/node-macro/src/derive_choice_type.rs +++ b/node-graph/node-macro/src/derive_choice_type.rs @@ -144,16 +144,6 @@ fn derive_enum(enum_attrs: &[Attribute], name: Ident, input: syn::DataEnum) -> s quote! { &[ #(#items)* ], } }) .collect(); - let display_arm: Vec<_> = variants - .iter() - .map(|vg| vg.iter()) - .flatten() - .map(|v| { - let vn = &v.name; - let vl = &v.basic_item.label; - quote! { #name::#vn => write!(f, #vl), } - }) - .collect(); let widget_hint = match enum_info.widget_hint { WidgetHint::Radio => quote! { RadioButtons }, WidgetHint::Dropdown => quote! { Dropdown }, @@ -171,14 +161,6 @@ fn derive_enum(enum_attrs: &[Attribute], name: Ident, input: syn::DataEnum) -> s &[ #(#group)* ] } } - - impl core::fmt::Display for #name { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - #( #display_arm )* - } - } - } }) } diff --git a/node-graph/node-macro/src/lib.rs b/node-graph/node-macro/src/lib.rs index 706d4196b8..9c7627c9d6 100644 --- a/node-graph/node-macro/src/lib.rs +++ b/node-graph/node-macro/src/lib.rs @@ -18,7 +18,7 @@ mod validation; /// /// `#[widget(F)]` on a type indicates the type of widget to use to display/edit the type, currently `Radio` and `Dropdown` are supported. /// -/// `#[label("Foo")]` on a variant overrides the default UI label (which is otherwise the name converted to title case). Default or not, labels are collected as the [`core::fmt::Display`] implementation. +/// `#[label("Foo")]` on a variant overrides the default UI label (which is otherwise the name converted to title case). /// `#[icon("tag"))]` sets the icon to use when a variant is shown in a menu or radio button. /// Doc comments on a variant become tooltip text. #[proc_macro_derive(ChoiceType, attributes(widget, menu_separator, label, icon))] From fbe60cb7d52779f02b3862664fd71210f8ceae8d Mon Sep 17 00:00:00 2001 From: Kythyria Tieran Date: Wed, 23 Apr 2025 23:19:48 +0100 Subject: [PATCH 11/18] Use ChoiceType, part 2 --- .../document/node_graph/node_properties.rs | 203 +++--------------- node-graph/gcore/src/animation.rs | 16 +- node-graph/gcore/src/ops.rs | 9 +- node-graph/gcore/src/raster/adjustments.rs | 143 +++--------- node-graph/gcore/src/registry.rs | 1 + node-graph/gcore/src/vector/style.rs | 24 +-- .../node-macro/src/derive_choice_type.rs | 28 +++ node-graph/node-macro/src/lib.rs | 2 +- 8 files changed, 96 insertions(+), 330 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 8ebeea70e9..04a71295d3 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -112,6 +112,7 @@ trait ChoiceSource { fn from_tagged_value(tv: Option<&TaggedValue>) -> Option; fn enumerate() -> impl Iterator>; fn widget_hint() -> ChoiceWidgetHint; + fn description() -> Option<&'static str>; } struct ChoiceSourceStatic(std::marker::PhantomData); @@ -139,6 +140,9 @@ where fn widget_hint() -> ChoiceWidgetHint { T::WIDGET_HINT } + fn description() -> Option<&'static str> { + T::DESCRIPTION + } } fn enum_source() -> ChoiceSourceStatic { @@ -157,15 +161,19 @@ fn choice_widget(list: E, parameter_widgets_info: ParameterWidg if let Some(current) = E::from_tagged_value(input.as_non_exposed_value()) { let widget = match E::widget_hint() { - ChoiceWidgetHint::Dropdown => dropdown(list, node_id, index, current), - ChoiceWidgetHint::RadioButtons => radio_buttons(list, node_id, index, current), + ChoiceWidgetHint::Dropdown => dropdown(list, node_id, index, current).widget_holder(), + ChoiceWidgetHint::RadioButtons => radio_buttons(list, node_id, index, current).widget_holder(), }; widgets.extend_from_slice(&[Separator::new(SeparatorType::Unrelated).widget_holder(), widget]); } - LayoutGroup::Row { widgets } + let mut row = LayoutGroup::Row { widgets }; + if let Some(desc) = E::description() { + row = row.with_tooltip(desc); + } + row } -fn radio_buttons(_list: E, node_id: NodeId, index: usize, current: E::Value) -> WidgetHolder { +fn radio_buttons(_list: E, node_id: NodeId, index: usize, current: E::Value) -> RadioInput { let items = E::enumerate() .into_iter() .flatten() @@ -183,10 +191,10 @@ fn radio_buttons(_list: E, node_id: NodeId, index: usize, curre }) .collect(); - RadioInput::new(items).selected_index(E::into_index(current)).widget_holder() + RadioInput::new(items).selected_index(E::into_index(current)) } -fn dropdown(_list: E, node_id: NodeId, index: usize, current: E::Value) -> WidgetHolder { +fn dropdown(_list: E, node_id: NodeId, index: usize, current: E::Value) -> DropdownInput { let items = E::enumerate() .into_iter() .map(|category| { @@ -202,7 +210,7 @@ fn dropdown(_list: E, node_id: NodeId, index: usize, current: E }) .collect(); - DropdownInput::new(items).selected_index(E::into_index(current)).widget_holder() + DropdownInput::new(items).selected_index(E::into_index(current)) } pub(crate) fn property_from_type( @@ -277,11 +285,11 @@ pub(crate) fn property_from_type( Some(x) if x == TypeId::of::() => group_widget(default_info).into(), Some(x) if x == TypeId::of::() => footprint_widget(default_info, &mut extra_widgets), Some(x) if x == TypeId::of::() => blend_mode_widget(default_info), - Some(x) if x == TypeId::of::() => real_time_mode_widget(default_info), - Some(x) if x == TypeId::of::() => rgb_widget(default_info), - Some(x) if x == TypeId::of::() => rgba_widget(default_info), - Some(x) if x == TypeId::of::() => xy_widget(default_info), - Some(x) if x == TypeId::of::() => noise_type_widget(default_info), + Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), + Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), + Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), + Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), + Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), Some(x) if x == TypeId::of::() => fractal_type_widget(default_info, false), Some(x) if x == TypeId::of::() => cellular_distance_function_widget(default_info, false), Some(x) if x == TypeId::of::() => cellular_return_type_widget(default_info, false), @@ -841,109 +849,8 @@ pub fn number_widget(parameter_widgets_info: ParameterWidgetsInfo, number_props: widgets } -// TODO: Generalize this instead of using a separate function per dropdown menu enum -pub fn rgb_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup { - let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; - - let mut widgets = start_widgets(parameter_widgets_info, FrontendGraphDataType::General); - let Some(input) = document_node.inputs.get(index) else { - log::warn!("A widget failed to be built because its node's input index is invalid."); - return LayoutGroup::Row { widgets: vec![] }; - }; - if let Some(&TaggedValue::RedGreenBlue(mode)) = input.as_non_exposed_value() { - let calculation_modes = [RedGreenBlue::Red, RedGreenBlue::Green, RedGreenBlue::Blue]; - let mut entries = Vec::with_capacity(calculation_modes.len()); - for method in calculation_modes { - entries.push( - MenuListEntry::new(format!("{method:?}")) - .label(method.to_string()) - .on_update(update_value(move |_| TaggedValue::RedGreenBlue(method), node_id, index)) - .on_commit(commit_value), - ); - } - let entries = vec![entries]; - - widgets.extend_from_slice(&[ - Separator::new(SeparatorType::Unrelated).widget_holder(), - DropdownInput::new(entries).selected_index(Some(mode as u32)).widget_holder(), - ]); - } - LayoutGroup::Row { widgets }.with_tooltip("Color Channel") -} - -pub fn real_time_mode_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup { - let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; - - let mut widgets = start_widgets(parameter_widgets_info, FrontendGraphDataType::General); - let Some(input) = document_node.inputs.get(index) else { - log::warn!("A widget failed to be built because its node's input index is invalid."); - return LayoutGroup::Row { widgets: vec![] }; - }; - if let Some(&TaggedValue::RealTimeMode(mode)) = input.as_non_exposed_value() { - let calculation_modes = [ - RealTimeMode::Utc, - RealTimeMode::Year, - RealTimeMode::Hour, - RealTimeMode::Minute, - RealTimeMode::Second, - RealTimeMode::Millisecond, - ]; - let mut entries = Vec::with_capacity(calculation_modes.len()); - for method in calculation_modes { - entries.push( - MenuListEntry::new(format!("{method:?}")) - .label(method.to_string()) - .on_update(update_value(move |_| TaggedValue::RealTimeMode(method), node_id, index)) - .on_commit(commit_value), - ); - } - let entries = vec![entries]; - - widgets.extend_from_slice(&[ - Separator::new(SeparatorType::Unrelated).widget_holder(), - DropdownInput::new(entries).selected_index(Some(mode as u32)).widget_holder(), - ]); - } - LayoutGroup::Row { widgets }.with_tooltip("Real Time Mode") -} - -pub fn rgba_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup { - let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; - - let mut widgets = start_widgets(parameter_widgets_info, FrontendGraphDataType::General); - let Some(input) = document_node.inputs.get(index) else { - log::warn!("A widget failed to be built because its node's input index is invalid."); - return LayoutGroup::Row { widgets: vec![] }; - }; - if let Some(&TaggedValue::RedGreenBlueAlpha(mode)) = input.as_non_exposed_value() { - let calculation_modes = [RedGreenBlueAlpha::Red, RedGreenBlueAlpha::Green, RedGreenBlueAlpha::Blue, RedGreenBlueAlpha::Alpha]; - let mut entries = Vec::with_capacity(calculation_modes.len()); - for method in calculation_modes { - entries.push( - MenuListEntry::new(format!("{method:?}")) - .label(method.to_string()) - .on_update(update_value(move |_| TaggedValue::RedGreenBlueAlpha(method), node_id, index)) - .on_commit(commit_value), - ); - } - let entries = vec![entries]; - - widgets.extend_from_slice(&[ - Separator::new(SeparatorType::Unrelated).widget_holder(), - DropdownInput::new(entries).selected_index(Some(mode as u32)).widget_holder(), - ]); - } - LayoutGroup::Row { widgets }.with_tooltip("Color Channel") -} - -pub fn xy_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup { - // TODO: Put tooltips in choice_widget - choice_widget(enum_source::(), parameter_widgets_info).with_tooltip("X or Y Component of Vector2") -} - pub fn noise_type_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup { - // TODO: Put tooltips in choice_widget - choice_widget(enum_source::(), parameter_widgets_info).with_tooltip("Style of noise pattern") + choice_widget(enum_source::(), parameter_widgets_info) } // TODO: Generalize this instead of using a separate function per dropdown menu enum @@ -956,19 +863,9 @@ pub fn fractal_type_widget(parameter_widgets_info: ParameterWidgetsInfo, disable return LayoutGroup::Row { widgets: vec![] }; }; if let Some(&TaggedValue::FractalType(fractal_type)) = input.as_non_exposed_value() { - let entries = FractalType::list() - .iter() - .map(|fractal_type| { - MenuListEntry::new(format!("{fractal_type:?}")) - .label(fractal_type.to_string()) - .on_update(update_value(move |_| TaggedValue::FractalType(*fractal_type), node_id, index)) - .on_commit(commit_value) - }) - .collect(); - widgets.extend_from_slice(&[ Separator::new(SeparatorType::Unrelated).widget_holder(), - DropdownInput::new(vec![entries]).selected_index(Some(fractal_type as u32)).disabled(disabled).widget_holder(), + dropdown(enum_source::(), node_id, index, fractal_type).disabled(disabled).widget_holder(), ]); } LayoutGroup::Row { widgets }.with_tooltip("Style of layered levels of the noise pattern") @@ -984,20 +881,9 @@ pub fn cellular_distance_function_widget(parameter_widgets_info: ParameterWidget return LayoutGroup::Row { widgets: vec![] }; }; if let Some(&TaggedValue::CellularDistanceFunction(cellular_distance_function)) = input.as_non_exposed_value() { - let entries = CellularDistanceFunction::list() - .iter() - .map(|cellular_distance_function| { - MenuListEntry::new(format!("{cellular_distance_function:?}")) - .label(cellular_distance_function.to_string()) - .on_update(update_value(move |_| TaggedValue::CellularDistanceFunction(*cellular_distance_function), node_id, index)) - .on_commit(commit_value) - }) - .collect(); - widgets.extend_from_slice(&[ Separator::new(SeparatorType::Unrelated).widget_holder(), - DropdownInput::new(vec![entries]) - .selected_index(Some(cellular_distance_function as u32)) + dropdown(enum_source::(), node_id, index, cellular_distance_function) .disabled(disabled) .widget_holder(), ]); @@ -1015,19 +901,9 @@ pub fn cellular_return_type_widget(parameter_widgets_info: ParameterWidgetsInfo, return LayoutGroup::Row { widgets: vec![] }; }; if let Some(&TaggedValue::CellularReturnType(cellular_return_type)) = input.as_non_exposed_value() { - let entries = CellularReturnType::list() - .iter() - .map(|cellular_return_type| { - MenuListEntry::new(format!("{cellular_return_type:?}")) - .label(cellular_return_type.to_string()) - .on_update(update_value(move |_| TaggedValue::CellularReturnType(*cellular_return_type), node_id, index)) - .on_commit(commit_value) - }) - .collect(); - widgets.extend_from_slice(&[ Separator::new(SeparatorType::Unrelated).widget_holder(), - DropdownInput::new(vec![entries]).selected_index(Some(cellular_return_type as u32)).disabled(disabled).widget_holder(), + dropdown(enum_source::(), node_id, index, cellular_return_type).disabled(disabled).widget_holder(), ]); } LayoutGroup::Row { widgets }.with_tooltip("Return type of the cellular noise") @@ -1043,21 +919,9 @@ pub fn domain_warp_type_widget(parameter_widgets_info: ParameterWidgetsInfo, dis return LayoutGroup::Row { widgets: vec![] }; }; if let Some(&TaggedValue::DomainWarpType(domain_warp_type)) = input.as_non_exposed_value() { - let entries = DomainWarpType::list() - .iter() - .map(|i| i.into_iter()) - .flatten() - .map(|(item, info)| { - MenuListEntry::new(info.name.as_ref()) - .label(info.label.as_ref()) - .on_update(update_value(move |_| TaggedValue::DomainWarpType(*item), node_id, index)) - .on_commit(commit_value) - }) - .collect(); - widgets.extend_from_slice(&[ Separator::new(SeparatorType::Unrelated).widget_holder(), - DropdownInput::new(vec![entries]).selected_index(Some(domain_warp_type as u32)).disabled(disabled).widget_holder(), + dropdown(enum_source::(), node_id, index, domain_warp_type).disabled(disabled).widget_holder(), ]); } LayoutGroup::Row { widgets }.with_tooltip("Type of domain warp") @@ -1385,22 +1249,7 @@ pub(crate) fn selective_color_properties(node_id: NodeId, context: &mut NodeProp return vec![]; }; if let Some(&TaggedValue::SelectiveColorChoice(choice)) = input.as_non_exposed_value() { - use SelectiveColorChoice::*; - let entries = [[Reds, Yellows, Greens, Cyans, Blues, Magentas].as_slice(), [Whites, Neutrals, Blacks].as_slice()] - .into_iter() - .map(|section| { - section - .iter() - .map(|choice| { - MenuListEntry::new(format!("{choice:?}")) - .label(choice.to_string()) - .on_update(update_value(move |_| TaggedValue::SelectiveColorChoice(*choice), node_id, colors_index)) - .on_commit(commit_value) - }) - .collect() - }) - .collect(); - colors.extend([DropdownInput::new(entries).selected_index(Some(choice as u32)).widget_holder()]); + colors.extend([radio_buttons(enum_source::(), node_id, colors_index, choice).widget_holder()]); } let colors_choice_index = match &document_node.inputs[colors_index].as_value() { diff --git a/node-graph/gcore/src/animation.rs b/node-graph/gcore/src/animation.rs index adb63c6c0f..8bbdc8b4de 100644 --- a/node-graph/gcore/src/animation.rs +++ b/node-graph/gcore/src/animation.rs @@ -2,9 +2,11 @@ use crate::{Ctx, ExtractAnimationTime, ExtractTime}; const DAY: f64 = 1000. * 3600. * 24.; -#[derive(Debug, Clone, Copy, PartialEq, Eq, dyn_any::DynAny, Default, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, dyn_any::DynAny, Default, Hash, node_macro::ChoiceType)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +/// Real Time Mode pub enum RealTimeMode { + #[label("UTC")] Utc, Year, Hour, @@ -13,18 +15,6 @@ pub enum RealTimeMode { Second, Millisecond, } -impl core::fmt::Display for RealTimeMode { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - RealTimeMode::Utc => write!(f, "UTC"), - RealTimeMode::Year => write!(f, "Year"), - RealTimeMode::Hour => write!(f, "Hour"), - RealTimeMode::Minute => write!(f, "Minute"), - RealTimeMode::Second => write!(f, "Second"), - RealTimeMode::Millisecond => write!(f, "Millisecond"), - } - } -} #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AnimationTimeMode { diff --git a/node-graph/gcore/src/ops.rs b/node-graph/gcore/src/ops.rs index a91fb3eb07..831c9b9390 100644 --- a/node-graph/gcore/src/ops.rs +++ b/node-graph/gcore/src/ops.rs @@ -505,19 +505,12 @@ fn extract_xy>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2 #[cfg_attr(feature = "std", derive(specta::Type))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] #[widget(Dropdown)] +/// X or Y Component of Vector2 pub enum XY { #[default] X, Y, } -impl core::fmt::Display for XY { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - XY::X => write!(f, "X"), - XY::Y => write!(f, "Y"), - } - } -} // TODO: Rename to "Passthrough" /// Passes-through the input value without changing it. This is useful for rerouting wires for organization purposes. diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index 3d83d4b917..053300a24d 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -823,7 +823,8 @@ async fn vibrance>( #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +/// Color Channel pub enum RedGreenBlue { #[default] Red, @@ -831,19 +832,10 @@ pub enum RedGreenBlue { Blue, } -impl core::fmt::Display for RedGreenBlue { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - RedGreenBlue::Red => write!(f, "Red"), - RedGreenBlue::Green => write!(f, "Green"), - RedGreenBlue::Blue => write!(f, "Blue"), - } - } -} - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +/// Color Channel pub enum RedGreenBlueAlpha { #[default] Red, @@ -852,21 +844,11 @@ pub enum RedGreenBlueAlpha { Alpha, } -impl core::fmt::Display for RedGreenBlueAlpha { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - RedGreenBlueAlpha::Red => write!(f, "Red"), - RedGreenBlueAlpha::Green => write!(f, "Green"), - RedGreenBlueAlpha::Blue => write!(f, "Blue"), - RedGreenBlueAlpha::Alpha => write!(f, "Alpha"), - } - } -} - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] +/// Style of noise pattern pub enum NoiseType { #[default] Perlin, @@ -885,122 +867,59 @@ pub enum NoiseType { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +/// Style of layered levels of the noise pattern pub enum FractalType { #[default] None, + #[label("Fractional Brownian Motion")] FBm, Ridged, PingPong, + #[label("Progressive (Domain Warp Only)")] DomainWarpProgressive, + #[label("Independent (Domain Warp Only)")] DomainWarpIndependent, } -impl core::fmt::Display for FractalType { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - FractalType::None => write!(f, "None"), - FractalType::FBm => write!(f, "Fractional Brownian Motion"), - FractalType::Ridged => write!(f, "Ridged"), - FractalType::PingPong => write!(f, "Ping Pong"), - FractalType::DomainWarpProgressive => write!(f, "Progressive (Domain Warp Only)"), - FractalType::DomainWarpIndependent => write!(f, "Independent (Domain Warp Only)"), - } - } -} - -impl FractalType { - pub fn list() -> &'static [FractalType; 6] { - &[ - FractalType::None, - FractalType::FBm, - FractalType::Ridged, - FractalType::PingPong, - FractalType::DomainWarpProgressive, - FractalType::DomainWarpIndependent, - ] - } -} - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +/// Distance function used by the cellular noise pub enum CellularDistanceFunction { #[default] Euclidean, + #[label("Euclidean Squared (Faster)")] EuclideanSq, Manhattan, Hybrid, } -impl core::fmt::Display for CellularDistanceFunction { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - CellularDistanceFunction::Euclidean => write!(f, "Euclidean"), - CellularDistanceFunction::EuclideanSq => write!(f, "Euclidean Squared (Faster)"), - CellularDistanceFunction::Manhattan => write!(f, "Manhattan"), - CellularDistanceFunction::Hybrid => write!(f, "Hybrid"), - } - } -} - -impl CellularDistanceFunction { - pub fn list() -> &'static [CellularDistanceFunction; 4] { - &[ - CellularDistanceFunction::Euclidean, - CellularDistanceFunction::EuclideanSq, - CellularDistanceFunction::Manhattan, - CellularDistanceFunction::Hybrid, - ] - } -} - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] pub enum CellularReturnType { CellValue, #[default] + #[label("Nearest (F1)")] Nearest, + #[label("Next Nearest (F2)")] NextNearest, + #[label("Average (F1 / 2 + F2 / 2)")] Average, + #[label("Difference (F2 - F1)")] Difference, + #[label("Product (F2 * F1 / 2)")] Product, + #[label("Division (F1 / F2)")] Division, } -impl core::fmt::Display for CellularReturnType { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - CellularReturnType::CellValue => write!(f, "Cell Value"), - CellularReturnType::Nearest => write!(f, "Nearest (F1)"), - CellularReturnType::NextNearest => write!(f, "Next Nearest (F2)"), - CellularReturnType::Average => write!(f, "Average (F1 / 2 + F2 / 2)"), - CellularReturnType::Difference => write!(f, "Difference (F2 - F1)"), - CellularReturnType::Product => write!(f, "Product (F2 * F1 / 2)"), - CellularReturnType::Division => write!(f, "Division (F1 / F2)"), - } - } -} - -impl CellularReturnType { - pub fn list() -> &'static [CellularReturnType; 7] { - &[ - CellularReturnType::CellValue, - CellularReturnType::Nearest, - CellularReturnType::NextNearest, - CellularReturnType::Average, - CellularReturnType::Difference, - CellularReturnType::Product, - CellularReturnType::Division, - ] - } -} - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] +/// Type of domain warp pub enum DomainWarpType { #[default] None, @@ -1132,7 +1051,7 @@ impl core::fmt::Display for RelativeAbsolute { #[repr(C)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] pub enum SelectiveColorChoice { #[default] Reds, @@ -1141,27 +1060,13 @@ pub enum SelectiveColorChoice { Cyans, Blues, Magentas, + + #[menu_separator] Whites, Neutrals, Blacks, } -impl core::fmt::Display for SelectiveColorChoice { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - SelectiveColorChoice::Reds => write!(f, "Reds"), - SelectiveColorChoice::Yellows => write!(f, "Yellows"), - SelectiveColorChoice::Greens => write!(f, "Greens"), - SelectiveColorChoice::Cyans => write!(f, "Cyans"), - SelectiveColorChoice::Blues => write!(f, "Blues"), - SelectiveColorChoice::Magentas => write!(f, "Magentas"), - SelectiveColorChoice::Whites => write!(f, "Whites"), - SelectiveColorChoice::Neutrals => write!(f, "Neutrals"), - SelectiveColorChoice::Blacks => write!(f, "Blacks"), - } - } -} - // Aims for interoperable compatibility with: // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27selc%27%20%3D%20Selective%20color // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=from%20%2D100...100.%20.-,Selective%20Color,-Selective%20Color%20settings diff --git a/node-graph/gcore/src/registry.rs b/node-graph/gcore/src/registry.rs index b1c0c97469..95b12c6547 100644 --- a/node-graph/gcore/src/registry.rs +++ b/node-graph/gcore/src/registry.rs @@ -56,6 +56,7 @@ pub struct FieldMetadata { pub trait ChoiceTypeStatic: Sized + Copy + crate::vector::misc::AsU32 + Send + Sync { const WIDGET_HINT: ChoiceWidgetHint; + const DESCRIPTION: Option<&'static str>; fn list() -> &'static [&'static [(Self, VariantMetadata)]]; } diff --git a/node-graph/gcore/src/vector/style.rs b/node-graph/gcore/src/vector/style.rs index bc42df21c6..5f3fa42223 100644 --- a/node-graph/gcore/src/vector/style.rs +++ b/node-graph/gcore/src/vector/style.rs @@ -480,12 +480,12 @@ pub enum LineCap { Square, } -impl Display for LineCap { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl LineCap { + fn svg_name(&self) -> &'static str { match self { - LineCap::Butt => write!(f, "butt"), - LineCap::Round => write!(f, "round"), - LineCap::Square => write!(f, "square"), + LineCap::Butt => "butt", + LineCap::Round => "round", + LineCap::Square => "square", } } } @@ -500,12 +500,12 @@ pub enum LineJoin { Round, } -impl Display for LineJoin { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl LineJoin { + fn svg_name(&self) -> &'static str { match self { - LineJoin::Bevel => write!(f, "bevel"), - LineJoin::Miter => write!(f, "miter"), - LineJoin::Round => write!(f, "round"), + LineJoin::Bevel => "bevel", + LineJoin::Miter => "miter", + LineJoin::Round => "round", } } } @@ -654,10 +654,10 @@ impl Stroke { let _ = write!(&mut attributes, r#" stroke-dashoffset="{}""#, dash_offset); } if let Some(line_cap) = line_cap { - let _ = write!(&mut attributes, r#" stroke-linecap="{}""#, line_cap); + let _ = write!(&mut attributes, r#" stroke-linecap="{}""#, line_cap.svg_name()); } if let Some(line_join) = line_join { - let _ = write!(&mut attributes, r#" stroke-linejoin="{}""#, line_join); + let _ = write!(&mut attributes, r#" stroke-linejoin="{}""#, line_join.svg_name()); } if let Some(line_join_miter_limit) = line_join_miter_limit { let _ = write!(&mut attributes, r#" stroke-miterlimit="{}""#, line_join_miter_limit); diff --git a/node-graph/node-macro/src/derive_choice_type.rs b/node-graph/node-macro/src/derive_choice_type.rs index 933c3f041a..dc9cbed8ac 100644 --- a/node-graph/node-macro/src/derive_choice_type.rs +++ b/node-graph/node-macro/src/derive_choice_type.rs @@ -98,6 +98,16 @@ fn derive_enum(enum_attrs: &[Attribute], name: Ident, input: syn::DataEnum) -> s variants.last_mut().unwrap().push(Variant { name: va.ident.clone(), basic_item }) } + let display_arm: Vec<_> = variants + .iter() + .map(|vg| vg.iter()) + .flatten() + .map(|v| { + let vn = &v.name; + let vl = &v.basic_item.label; + quote! { #name::#vn => write!(f, #vl), } + }) + .collect(); let crate_name = proc_macro_crate::crate_name("graphene-core").map_err(|e| { syn::Error::new( @@ -113,6 +123,15 @@ fn derive_enum(enum_attrs: &[Attribute], name: Ident, input: syn::DataEnum) -> s } }; + let enum_description = match &enum_info.basic_item.description { + Some(s) => { + let s = s.trim(); + quote! { Some(#s) } + } + None => { + quote! { None } + } + }; let group: Vec<_> = variants .iter() .map(|vg| { @@ -157,10 +176,19 @@ fn derive_enum(enum_attrs: &[Attribute], name: Ident, input: syn::DataEnum) -> s impl #crate_name::registry::ChoiceTypeStatic for #name { const WIDGET_HINT: #crate_name::registry::ChoiceWidgetHint = #crate_name::registry::ChoiceWidgetHint::#widget_hint; + const DESCRIPTION: Option<&'static str> = #enum_description; fn list() -> &'static [&'static [(Self, #crate_name::registry::VariantMetadata)]] { &[ #(#group)* ] } } + + impl core::fmt::Display for #name { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + #( #display_arm )* + } + } + } }) } diff --git a/node-graph/node-macro/src/lib.rs b/node-graph/node-macro/src/lib.rs index 9c7627c9d6..44d8f7e457 100644 --- a/node-graph/node-macro/src/lib.rs +++ b/node-graph/node-macro/src/lib.rs @@ -18,7 +18,7 @@ mod validation; /// /// `#[widget(F)]` on a type indicates the type of widget to use to display/edit the type, currently `Radio` and `Dropdown` are supported. /// -/// `#[label("Foo")]` on a variant overrides the default UI label (which is otherwise the name converted to title case). +/// `#[label("Foo")]` on a variant overrides the default UI label (which is otherwise the name converted to title case). All labels are collected into a [`core::fmt::Display`] impl. /// `#[icon("tag"))]` sets the icon to use when a variant is shown in a menu or radio button. /// Doc comments on a variant become tooltip text. #[proc_macro_derive(ChoiceType, attributes(widget, menu_separator, label, icon))] From d737a4dcea6e0e1f9cd5ae2aeb8596bb4ed072e6 Mon Sep 17 00:00:00 2001 From: Kythyria Tieran Date: Sat, 26 Apr 2025 06:37:47 +0100 Subject: [PATCH 12/18] Introduce a builder API for choice widgets --- .../document/node_graph/node_properties.rs | 166 +++++++++++++++++- 1 file changed, 165 insertions(+), 1 deletion(-) diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 04a71295d3..7f7b4be5b7 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -303,7 +303,7 @@ pub(crate) fn property_from_type( Some(x) if x == TypeId::of::() => gradient_type_widget(default_info), Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), - Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), + Some(x) if x == TypeId::of::() => choice::enum_choice::().for_socket(default_info).property_row(), _ => { let mut widgets = start_widgets(default_info, FrontendGraphDataType::General); widgets.extend_from_slice(&[ @@ -332,6 +332,170 @@ pub(crate) fn property_from_type( Ok(extra_widgets) } +pub mod choice { + use super::ParameterWidgetsInfo; + use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType; + use crate::messages::tool::tool_messages::tool_prelude::*; + use graph_craft::document::value::TaggedValue; + use graphene_core::registry::{ChoiceTypeStatic, ChoiceWidgetHint, VariantMetadata}; + use std::marker::PhantomData; + + trait WidgetFactory { + type Value: Clone + 'static; + + fn disabled(self, disabled: bool) -> Self; + fn build(&self, current: Self::Value, updater_factory: impl Fn() -> U, committer_factory: impl Fn() -> C) -> WidgetHolder + where + U: Fn(&Self::Value) -> Message + 'static + Send + Sync, + C: Fn(&()) -> Message + 'static + Send + Sync; + fn description(&self) -> Option<&str>; + } + + pub fn enum_choice() -> EnumChoice { + EnumChoice { + disabled: false, + phantom: PhantomData, + } + } + + pub struct EnumChoice { + disabled: bool, + phantom: PhantomData, + } + + impl EnumChoice { + pub fn for_socket(self, parameter_info: ParameterWidgetsInfo) -> ForSocket { + ForSocket { widget_factory: self, parameter_info } + } + pub fn for_value(self, current: E) -> ForValue { + todo!() + } + + pub fn into_menu_entries(self, action: impl Fn(E) -> Message + 'static + Send + Sync) -> Vec> { + todo!() + } + + fn dropdown(&self, current: E, updater_factory: impl Fn() -> U, committer_factory: impl Fn() -> C) -> WidgetHolder + where + U: Fn(&E) -> Message + 'static + Send + Sync, + C: Fn(&()) -> Message + 'static + Send + Sync, + { + let items = E::list() + .into_iter() + .map(|group| { + group + .into_iter() + .map(|(item, metadata)| { + let item = item.clone(); + let updater = updater_factory(); + let committer = committer_factory(); + MenuListEntry::new(metadata.name.as_ref()) + .label(metadata.label.as_ref()) + .on_update(move |_| updater(&item)) + .on_commit(committer) + }) + .collect() + }) + .collect(); + DropdownInput::new(items).disabled(self.disabled).selected_index(Some(current.as_u32())).widget_holder() + } + + fn radio_buttons(&self, current: E, updater_factory: impl Fn() -> U, committer_factory: impl Fn() -> C) -> WidgetHolder + where + U: Fn(&E) -> Message + 'static + Send + Sync, + C: Fn(&()) -> Message + 'static + Send + Sync, + { + let items = E::list() + .into_iter() + .map(|group| group.into_iter()) + .flatten() + .map(|(item, var_meta)| { + let item = item.clone(); + let updater = updater_factory(); + let committer = committer_factory(); + let red = RadioEntryData::new(var_meta.name.as_ref()).on_update(move |_| updater(&item)).on_commit(committer); + let red = match (var_meta.icon.as_deref(), var_meta.docstring.as_deref()) { + (None, None) => red.label(var_meta.label.as_ref()), + (None, Some(doc)) => red.label(var_meta.label.as_ref()).tooltip(doc), + (Some(icon), None) => red.icon(icon).tooltip(var_meta.label.as_ref()), + (Some(icon), Some(doc)) => red.icon(icon).tooltip(format!("{}\n\n{}", var_meta.label, doc)), + }; + red + }) + .collect(); + RadioInput::new(items).selected_index(Some(current.as_u32())).widget_holder() + } + } + impl WidgetFactory for EnumChoice { + type Value = E; + fn disabled(self, disabled: bool) -> Self { + Self { disabled, ..self } + } + + fn description(&self) -> Option<&str> { + E::DESCRIPTION + } + + fn build(&self, current: Self::Value, updater_factory: impl Fn() -> U, committer_factory: impl Fn() -> C) -> WidgetHolder + where + U: Fn(&Self::Value) -> Message + 'static + Send + Sync, + C: Fn(&()) -> Message + 'static + Send + Sync, + { + match E::WIDGET_HINT { + ChoiceWidgetHint::Dropdown => self.dropdown(current, updater_factory, committer_factory), + ChoiceWidgetHint::RadioButtons => self.radio_buttons(current, updater_factory, committer_factory), + } + } + } + + pub struct ForSocket<'p, W> { + widget_factory: W, + parameter_info: ParameterWidgetsInfo<'p>, + } + + impl<'p, W> ForSocket<'p, W> + where + W: WidgetFactory, + for<'a> &'a W::Value: TryFrom<&'a TaggedValue>, + W::Value: Clone, + TaggedValue: From, + { + fn disabled(self, disabled: bool) -> Self { + Self { + widget_factory: self.widget_factory.disabled(disabled), + ..self + } + } + + pub fn property_row(self) -> LayoutGroup { + let ParameterWidgetsInfo { document_node, node_id, index, .. } = self.parameter_info; + + let mut widgets = super::start_widgets(self.parameter_info, FrontendGraphDataType::General); + + let Some(input) = document_node.inputs.get(index) else { + log::warn!("A widget failed to be built because its node's input index is invalid."); + return LayoutGroup::Row { widgets: vec![] }; + }; + + let input: Option = input.as_non_exposed_value().and_then(|v| <&W::Value as TryFrom<&TaggedValue>>::try_from(v).ok()).map(Clone::clone); + + if let Some(current) = input { + let committer = || super::commit_value; + let updater = || super::update_value(move |v: &W::Value| TaggedValue::from(v.clone()), node_id, index); + let widget = self.widget_factory.build(current, updater, committer); + widgets.extend_from_slice(&[Separator::new(SeparatorType::Unrelated).widget_holder(), widget]); + } + let mut row = LayoutGroup::Row { widgets }; + if let Some(desc) = self.widget_factory.description() { + row = row.with_tooltip(desc); + } + row + } + } + + pub struct ForValue(PhantomData); +} + pub fn text_widget(parameter_widgets_info: ParameterWidgetsInfo) -> Vec { let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; From 9e7bd11c872d9d33f6c4fb97b06256c29bd4a537 Mon Sep 17 00:00:00 2001 From: Kythyria Tieran Date: Sun, 27 Apr 2025 08:41:54 +0100 Subject: [PATCH 13/18] Start using the new new API --- .../document/node_graph/node_properties.rs | 102 +++++------------- node-graph/gcore/src/raster/adjustments.rs | 2 +- 2 files changed, 28 insertions(+), 76 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 7f7b4be5b7..182ee69561 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -285,25 +285,25 @@ pub(crate) fn property_from_type( Some(x) if x == TypeId::of::() => group_widget(default_info).into(), Some(x) if x == TypeId::of::() => footprint_widget(default_info, &mut extra_widgets), Some(x) if x == TypeId::of::() => blend_mode_widget(default_info), - Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), - Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), - Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), - Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), - Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => fractal_type_widget(default_info, false), Some(x) if x == TypeId::of::() => cellular_distance_function_widget(default_info, false), Some(x) if x == TypeId::of::() => cellular_return_type_widget(default_info, false), Some(x) if x == TypeId::of::() => domain_warp_type_widget(default_info, false), Some(x) if x == TypeId::of::() => relative_absolute_widget(default_info), - Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), - Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), - Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), - Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => fill_type_widget(default_info), Some(x) if x == TypeId::of::() => gradient_type_widget(default_info), - Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), - Some(x) if x == TypeId::of::() => choice_widget(enum_source::(), default_info), - Some(x) if x == TypeId::of::() => choice::enum_choice::().for_socket(default_info).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), _ => { let mut widgets = start_widgets(default_info, FrontendGraphDataType::General); widgets.extend_from_slice(&[ @@ -370,6 +370,9 @@ pub mod choice { pub fn for_value(self, current: E) -> ForValue { todo!() } + pub fn disabled(self, disabled: bool) -> Self { + Self { disabled, ..self } + } pub fn into_menu_entries(self, action: impl Fn(E) -> Message + 'static + Send + Sync) -> Vec> { todo!() @@ -460,7 +463,7 @@ pub mod choice { W::Value: Clone, TaggedValue: From, { - fn disabled(self, disabled: bool) -> Self { + pub fn disabled(self, disabled: bool) -> Self { Self { widget_factory: self.widget_factory.disabled(disabled), ..self @@ -496,6 +499,8 @@ pub mod choice { pub struct ForValue(PhantomData); } +use choice::enum_choice; + pub fn text_widget(parameter_widgets_info: ParameterWidgetsInfo) -> Vec { let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; @@ -1014,81 +1019,28 @@ pub fn number_widget(parameter_widgets_info: ParameterWidgetsInfo, number_props: } pub fn noise_type_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup { - choice_widget(enum_source::(), parameter_widgets_info) + enum_choice::().for_socket(parameter_widgets_info).property_row() } // TODO: Generalize this instead of using a separate function per dropdown menu enum pub fn fractal_type_widget(parameter_widgets_info: ParameterWidgetsInfo, disabled: bool) -> LayoutGroup { - let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; - - let mut widgets = start_widgets(parameter_widgets_info, FrontendGraphDataType::General); - let Some(input) = document_node.inputs.get(index) else { - log::warn!("A widget failed to be built because its node's input index is invalid."); - return LayoutGroup::Row { widgets: vec![] }; - }; - if let Some(&TaggedValue::FractalType(fractal_type)) = input.as_non_exposed_value() { - widgets.extend_from_slice(&[ - Separator::new(SeparatorType::Unrelated).widget_holder(), - dropdown(enum_source::(), node_id, index, fractal_type).disabled(disabled).widget_holder(), - ]); - } - LayoutGroup::Row { widgets }.with_tooltip("Style of layered levels of the noise pattern") + enum_choice::().for_socket(parameter_widgets_info).disabled(disabled).property_row() } // TODO: Generalize this instead of using a separate function per dropdown menu enum pub fn cellular_distance_function_widget(parameter_widgets_info: ParameterWidgetsInfo, disabled: bool) -> LayoutGroup { - let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; - - let mut widgets = start_widgets(parameter_widgets_info, FrontendGraphDataType::General); - let Some(input) = document_node.inputs.get(index) else { - log::warn!("A widget failed to be built because its node's input index is invalid."); - return LayoutGroup::Row { widgets: vec![] }; - }; - if let Some(&TaggedValue::CellularDistanceFunction(cellular_distance_function)) = input.as_non_exposed_value() { - widgets.extend_from_slice(&[ - Separator::new(SeparatorType::Unrelated).widget_holder(), - dropdown(enum_source::(), node_id, index, cellular_distance_function) - .disabled(disabled) - .widget_holder(), - ]); - } - LayoutGroup::Row { widgets }.with_tooltip("Distance function used by the cellular noise") + enum_choice::().for_socket(parameter_widgets_info).disabled(disabled).property_row() } // TODO: Generalize this instead of using a separate function per dropdown menu enum pub fn cellular_return_type_widget(parameter_widgets_info: ParameterWidgetsInfo, disabled: bool) -> LayoutGroup { - let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; - let mut widgets = start_widgets(parameter_widgets_info, FrontendGraphDataType::General); - let Some(input) = document_node.inputs.get(index) else { - log::warn!("A widget failed to be built because its node's input index is invalid."); - return LayoutGroup::Row { widgets: vec![] }; - }; - if let Some(&TaggedValue::CellularReturnType(cellular_return_type)) = input.as_non_exposed_value() { - widgets.extend_from_slice(&[ - Separator::new(SeparatorType::Unrelated).widget_holder(), - dropdown(enum_source::(), node_id, index, cellular_return_type).disabled(disabled).widget_holder(), - ]); - } - LayoutGroup::Row { widgets }.with_tooltip("Return type of the cellular noise") + enum_choice::().for_socket(parameter_widgets_info).disabled(disabled).property_row() } // TODO: Generalize this instead of using a separate function per dropdown menu enum pub fn domain_warp_type_widget(parameter_widgets_info: ParameterWidgetsInfo, disabled: bool) -> LayoutGroup { - let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; - - let mut widgets = start_widgets(parameter_widgets_info, FrontendGraphDataType::General); - let Some(input) = document_node.inputs.get(index) else { - log::warn!("A widget failed to be built because its node's input index is invalid."); - return LayoutGroup::Row { widgets: vec![] }; - }; - if let Some(&TaggedValue::DomainWarpType(domain_warp_type)) = input.as_non_exposed_value() { - widgets.extend_from_slice(&[ - Separator::new(SeparatorType::Unrelated).widget_holder(), - dropdown(enum_source::(), node_id, index, domain_warp_type).disabled(disabled).widget_holder(), - ]); - } - LayoutGroup::Row { widgets }.with_tooltip("Type of domain warp") + enum_choice::().for_socket(parameter_widgets_info).disabled(disabled).property_row() } // TODO: Generalize this instead of using a separate function per dropdown menu enum @@ -1499,7 +1451,7 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte return Vec::new(); } }; - let grid_type = choice_widget(enum_source::(), ParameterWidgetsInfo::from_index(document_node, node_id, grid_type_index, true, context)); + let grid_type = enum_choice::().for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, grid_type_index, true, context)).property_row(); let mut widgets = vec![grid_type]; @@ -2014,8 +1966,8 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) - ); let number_input = NumberInput::default().unit(" px").disabled(dash_lengths_val.is_empty()); let dash_offset = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, dash_offset_index, true, context), number_input); - let line_cap = choice_widget(enum_source::(), ParameterWidgetsInfo::from_index(document_node, node_id, line_cap_index, true, context)); - let line_join = choice_widget(enum_source::(), ParameterWidgetsInfo::from_index(document_node, node_id, line_join_index, true, context)); + let line_cap = enum_choice::().for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, line_cap_index, true, context)).property_row(); + let line_join = enum_choice::().for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, line_join_index, true, context)).property_row(); let line_join_val = match &document_node.inputs[line_join_index].as_value() { Some(TaggedValue::LineJoin(x)) => x, _ => &LineJoin::Miter, @@ -2051,7 +2003,7 @@ pub fn offset_path_properties(node_id: NodeId, context: &mut NodePropertiesConte let number_input = NumberInput::default().unit(" px"); let distance = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, distance_index, true, context), number_input); - let line_join = choice_widget(enum_source::(), ParameterWidgetsInfo::from_index(document_node, node_id, line_join_index, true, context)); + let line_join = enum_choice::().for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, line_join_index, true, context)).property_row(); let line_join_val = match &document_node.inputs[line_join_index].as_value() { Some(TaggedValue::LineJoin(x)) => x, _ => &LineJoin::Miter, diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index 053300a24d..6d65812f19 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -847,7 +847,7 @@ pub enum RedGreenBlueAlpha { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] -#[widget(Radio)] +#[widget(Dropdown)] /// Style of noise pattern pub enum NoiseType { #[default] From ed108620d4123521ee772a0846ffca33d8137ca1 Mon Sep 17 00:00:00 2001 From: Kythyria Tieran Date: Sun, 27 Apr 2025 08:57:04 +0100 Subject: [PATCH 14/18] DomainWarpType should be a dropdown still --- node-graph/gcore/src/raster/adjustments.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index bb59e8625b..e914670e14 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -918,7 +918,7 @@ pub enum CellularReturnType { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] -#[widget(Radio)] +#[widget(Dropdown)] /// Type of domain warp pub enum DomainWarpType { #[default] From d64362f5df9513b438e92cb1aa68e663942b5a59 Mon Sep 17 00:00:00 2001 From: Kythyria Tieran Date: Wed, 30 Apr 2025 08:09:46 +0100 Subject: [PATCH 15/18] Handle the case where a node property can never have a socket --- .../document/node_graph/node_properties.rs | 142 ++++-------------- node-graph/gcore/src/vector/style.rs | 2 +- 2 files changed, 27 insertions(+), 117 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index e00a10340a..92adb421b5 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -16,7 +16,7 @@ use graphene_core::raster::{ BlendMode, CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute, SelectiveColorChoice, }; -use graphene_core::registry::{ChoiceTypeStatic, ChoiceWidgetHint, VariantMetadata}; +use graphene_core::registry::ChoiceTypeStatic; use graphene_core::text::Font; use graphene_core::vector::generator_nodes::grid; use graphene_core::vector::misc::CentroidType; @@ -86,7 +86,7 @@ pub fn add_blank_assist(widgets: &mut Vec) { ]); } -pub fn start_widgets(parameter_widgets_info: ParameterWidgetsInfo, data_type: FrontendGraphDataType) -> Vec { +pub fn start_widgets_exposable(parameter_widgets_info: ParameterWidgetsInfo, data_type: FrontendGraphDataType, exposable: bool) -> Vec { let ParameterWidgetsInfo { document_node, node_id, @@ -101,7 +101,11 @@ pub fn start_widgets(parameter_widgets_info: ParameterWidgetsInfo, data_type: Fr return vec![]; }; let description = if description != "TODO" { description } else { "" }; - let mut widgets = vec![expose_widget(node_id, index, data_type, input.is_exposed()), TextLabel::new(name).tooltip(description).widget_holder()]; + let mut widgets = Vec::with_capacity(6); + if exposable { + widgets.push(expose_widget(node_id, index, data_type, input.is_exposed())); + } + widgets.push(TextLabel::new(name).tooltip(description).widget_holder()); if blank_assist { add_blank_assist(&mut widgets); } @@ -109,113 +113,8 @@ pub fn start_widgets(parameter_widgets_info: ParameterWidgetsInfo, data_type: Fr widgets } -trait ChoiceSource { - type Value: Sized + Copy + Send + Sync + 'static; - - fn into_index(v: Self::Value) -> Option; - fn into_tagged_value(v: Self::Value) -> TaggedValue; - fn from_tagged_value(tv: Option<&TaggedValue>) -> Option; - fn enumerate() -> impl Iterator>; - fn widget_hint() -> ChoiceWidgetHint; - fn description() -> Option<&'static str>; -} - -struct ChoiceSourceStatic(std::marker::PhantomData); -impl ChoiceSource for ChoiceSourceStatic -where - T: ChoiceTypeStatic + 'static, - for<'a> &'a T: TryFrom<&'a TaggedValue>, - TaggedValue: From, -{ - type Value = T; - - fn into_index(v: Self::Value) -> Option { - Some(v.as_u32()) - } - fn into_tagged_value(v: Self::Value) -> TaggedValue { - TaggedValue::from(v) - } - fn from_tagged_value(tv: Option<&TaggedValue>) -> Option { - let v_ref: Option<&Self::Value> = tv.map(|tv| tv.try_into().ok()).flatten(); - v_ref.map(|vr| *vr) - } - fn enumerate() -> impl Iterator> { - T::list().into_iter().map(|i| i.into_iter()) - } - fn widget_hint() -> ChoiceWidgetHint { - T::WIDGET_HINT - } - fn description() -> Option<&'static str> { - T::DESCRIPTION - } -} - -fn enum_source() -> ChoiceSourceStatic { - ChoiceSourceStatic(std::marker::PhantomData) -} - -fn choice_widget(list: E, parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup { - let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; - - let mut widgets = start_widgets(parameter_widgets_info, FrontendGraphDataType::General); - - let Some(input) = document_node.inputs.get(index) else { - log::warn!("A widget failed to be built because its node's input index is invalid."); - return LayoutGroup::Row { widgets: vec![] }; - }; - - if let Some(current) = E::from_tagged_value(input.as_non_exposed_value()) { - let widget = match E::widget_hint() { - ChoiceWidgetHint::Dropdown => dropdown(list, node_id, index, current).widget_holder(), - ChoiceWidgetHint::RadioButtons => radio_buttons(list, node_id, index, current).widget_holder(), - }; - widgets.extend_from_slice(&[Separator::new(SeparatorType::Unrelated).widget_holder(), widget]); - } - let mut row = LayoutGroup::Row { widgets }; - if let Some(desc) = E::description() { - row = row.with_tooltip(desc); - } - row -} - -fn radio_buttons(_list: E, node_id: NodeId, index: usize, current: E::Value) -> RadioInput { - let items = E::enumerate() - .into_iter() - .flatten() - .map(|(item, var_meta)| { - let red = RadioEntryData::new(var_meta.name.as_ref()) - .on_update(update_value(move |_| E::into_tagged_value(*item), node_id, index)) - .on_commit(commit_value); - let red = match (var_meta.icon.as_deref(), var_meta.docstring.as_deref()) { - (None, None) => red.label(var_meta.label.as_ref()), - (None, Some(doc)) => red.label(var_meta.label.as_ref()).tooltip(doc), - (Some(icon), None) => red.icon(icon).tooltip(var_meta.label.as_ref()), - (Some(icon), Some(doc)) => red.icon(icon).tooltip(format!("{}\n\n{}", var_meta.label, doc)), - }; - red - }) - .collect(); - - RadioInput::new(items).selected_index(E::into_index(current)) -} - -fn dropdown(_list: E, node_id: NodeId, index: usize, current: E::Value) -> DropdownInput { - let items = E::enumerate() - .into_iter() - .map(|category| { - category - .into_iter() - .map(|(item, var_meta)| { - MenuListEntry::new(var_meta.name.as_ref()) - .label(var_meta.label.as_ref()) - .on_update(update_value(move |_| E::into_tagged_value(*item), node_id, index)) - .on_commit(commit_value) - }) - .collect() - }) - .collect(); - - DropdownInput::new(items).selected_index(E::into_index(current)) +pub fn start_widgets(parameter_widgets_info: ParameterWidgetsInfo, data_type: FrontendGraphDataType) -> Vec { + start_widgets_exposable(parameter_widgets_info, data_type, true) } pub(crate) fn property_from_type( @@ -346,7 +245,7 @@ pub mod choice { use graphene_core::registry::{ChoiceTypeStatic, ChoiceWidgetHint, VariantMetadata}; use std::marker::PhantomData; - trait WidgetFactory { + pub trait WidgetFactory { type Value: Clone + 'static; fn disabled(self, disabled: bool) -> Self; @@ -371,7 +270,7 @@ pub mod choice { impl EnumChoice { pub fn for_socket(self, parameter_info: ParameterWidgetsInfo) -> ForSocket { - ForSocket { widget_factory: self, parameter_info } + ForSocket { widget_factory: self, parameter_info, exposable: true } } pub fn for_value(self, current: E) -> ForValue { todo!() @@ -460,6 +359,7 @@ pub mod choice { pub struct ForSocket<'p, W> { widget_factory: W, parameter_info: ParameterWidgetsInfo<'p>, + exposable: bool, } impl<'p, W> ForSocket<'p, W> @@ -476,10 +376,17 @@ pub mod choice { } } + pub fn exposable(self, exposable: bool) -> Self { + Self { + exposable, + ..self + } + } + pub fn property_row(self) -> LayoutGroup { let ParameterWidgetsInfo { document_node, node_id, index, .. } = self.parameter_info; - let mut widgets = super::start_widgets(self.parameter_info, FrontendGraphDataType::General); + let mut widgets = super::start_widgets_exposable(self.parameter_info, FrontendGraphDataType::General, self.exposable); let Some(input) = document_node.inputs.get(index) else { log::warn!("A widget failed to be built because its node's input index is invalid."); @@ -1384,7 +1291,7 @@ pub(crate) fn selective_color_properties(node_id: NodeId, context: &mut NodeProp }; // Colors choice let colors_index = 38; - let mut colors = vec![TextLabel::new("Colors").widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder()]; + /*let mut colors = vec![TextLabel::new("Colors").widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder()]; add_blank_assist(&mut colors); let Some(input) = document_node.inputs.get(colors_index) else { @@ -1393,7 +1300,9 @@ pub(crate) fn selective_color_properties(node_id: NodeId, context: &mut NodeProp }; if let Some(&TaggedValue::SelectiveColorChoice(choice)) = input.as_non_exposed_value() { colors.extend([radio_buttons(enum_source::(), node_id, colors_index, choice).widget_holder()]); - } + }*/ + + let colours = enum_choice::().for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, colors_index, false, context)).exposable(false).property_row(); let colors_choice_index = match &document_node.inputs[colors_index].as_value() { Some(TaggedValue::SelectiveColorChoice(choice)) => choice, @@ -1446,7 +1355,8 @@ pub(crate) fn selective_color_properties(node_id: NodeId, context: &mut NodeProp vec![ // Colors choice - LayoutGroup::Row { widgets: colors }, + //LayoutGroup::Row { widgets: colors }, + colours, // CMYK LayoutGroup::Row { widgets: cyan }, LayoutGroup::Row { widgets: magenta }, diff --git a/node-graph/gcore/src/vector/style.rs b/node-graph/gcore/src/vector/style.rs index 5f3fa42223..86ca121d33 100644 --- a/node-graph/gcore/src/vector/style.rs +++ b/node-graph/gcore/src/vector/style.rs @@ -5,7 +5,7 @@ use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT}; use crate::renderer::format_transform_matrix; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; -use std::fmt::{self, Display, Write}; +use std::fmt::Write; #[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, specta::Type)] pub enum GradientType { From d51d7fb2413c257574f7b93a76316ab3fa1054dd Mon Sep 17 00:00:00 2001 From: Kythyria Tieran Date: Wed, 30 Apr 2025 08:11:42 +0100 Subject: [PATCH 16/18] Rustfmt --- .../document/node_graph/node_properties.rs | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 92adb421b5..876e0ea3f7 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -270,7 +270,11 @@ pub mod choice { impl EnumChoice { pub fn for_socket(self, parameter_info: ParameterWidgetsInfo) -> ForSocket { - ForSocket { widget_factory: self, parameter_info, exposable: true } + ForSocket { + widget_factory: self, + parameter_info, + exposable: true, + } } pub fn for_value(self, current: E) -> ForValue { todo!() @@ -377,10 +381,7 @@ pub mod choice { } pub fn exposable(self, exposable: bool) -> Self { - Self { - exposable, - ..self - } + Self { exposable, ..self } } pub fn property_row(self) -> LayoutGroup { @@ -968,7 +969,6 @@ pub fn cellular_distance_function_widget(parameter_widgets_info: ParameterWidget // TODO: Generalize this instead of using a separate function per dropdown menu enum pub fn cellular_return_type_widget(parameter_widgets_info: ParameterWidgetsInfo, disabled: bool) -> LayoutGroup { - enum_choice::().for_socket(parameter_widgets_info).disabled(disabled).property_row() } @@ -1302,7 +1302,10 @@ pub(crate) fn selective_color_properties(node_id: NodeId, context: &mut NodeProp colors.extend([radio_buttons(enum_source::(), node_id, colors_index, choice).widget_holder()]); }*/ - let colours = enum_choice::().for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, colors_index, false, context)).exposable(false).property_row(); + let colours = enum_choice::() + .for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, colors_index, false, context)) + .exposable(false) + .property_row(); let colors_choice_index = match &document_node.inputs[colors_index].as_value() { Some(TaggedValue::SelectiveColorChoice(choice)) => choice, @@ -1388,7 +1391,9 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte return Vec::new(); } }; - let grid_type = enum_choice::().for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, grid_type_index, true, context)).property_row(); + let grid_type = enum_choice::() + .for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, grid_type_index, true, context)) + .property_row(); let mut widgets = vec![grid_type]; @@ -1903,8 +1908,12 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) - ); let number_input = NumberInput::default().unit(" px").disabled(dash_lengths_val.is_empty()); let dash_offset = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, dash_offset_index, true, context), number_input); - let line_cap = enum_choice::().for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, line_cap_index, true, context)).property_row(); - let line_join = enum_choice::().for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, line_join_index, true, context)).property_row(); + let line_cap = enum_choice::() + .for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, line_cap_index, true, context)) + .property_row(); + let line_join = enum_choice::() + .for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, line_join_index, true, context)) + .property_row(); let line_join_val = match &document_node.inputs[line_join_index].as_value() { Some(TaggedValue::LineJoin(x)) => x, _ => &LineJoin::Miter, @@ -1940,7 +1949,9 @@ pub fn offset_path_properties(node_id: NodeId, context: &mut NodePropertiesConte let number_input = NumberInput::default().unit(" px"); let distance = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, distance_index, true, context), number_input); - let line_join = enum_choice::().for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, line_join_index, true, context)).property_row(); + let line_join = enum_choice::() + .for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, line_join_index, true, context)) + .property_row(); let line_join_val = match &document_node.inputs[line_join_index].as_value() { Some(TaggedValue::LineJoin(x)) => x, _ => &LineJoin::Miter, From be83c1954e3d845e12d3b6ce5f3809afeb9d3d58 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 1 May 2025 03:50:02 -0700 Subject: [PATCH 17/18] Code review --- .../node_graph/document_node_definitions.rs | 31 +- .../document/node_graph/node_properties.rs | 590 ++++++++---------- .../tool/tool_messages/gradient_tool.rs | 6 +- node-graph/gcore/src/animation.rs | 1 - node-graph/gcore/src/ops.rs | 2 +- node-graph/gcore/src/raster/adjustments.rs | 31 +- node-graph/gcore/src/registry.rs | 10 +- node-graph/gcore/src/vector/misc.rs | 7 +- node-graph/gcore/src/vector/style.rs | 6 +- node-graph/graph-craft/src/document/value.rs | 108 ++-- node-graph/gstd/src/raster.rs | 1 + .../node-macro/src/derive_choice_type.rs | 116 ++-- node-graph/node-macro/src/lib.rs | 2 + 13 files changed, 411 insertions(+), 500 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index 12f939a8b5..76fe7f5e93 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -1,3 +1,4 @@ +use super::node_properties::choice::enum_choice; use super::node_properties::{self, ParameterWidgetsInfo}; use super::utility_types::FrontendNodeType; use crate::messages::layout::utility_types::widget_prelude::*; @@ -3212,7 +3213,9 @@ fn static_input_properties() -> InputProperties { "noise_properties_noise_type".to_string(), Box::new(|node_id, index, context| { let (document_node, input_name, input_description) = node_properties::query_node_and_input_info(node_id, index, context)?; - let noise_type_row = node_properties::noise_type_widget(ParameterWidgetsInfo::new(document_node, node_id, index, input_name, input_description, true)); + let noise_type_row = enum_choice::() + .for_socket(ParameterWidgetsInfo::new(document_node, node_id, index, input_name, input_description, true)) + .property_row(); Ok(vec![noise_type_row, LayoutGroup::Row { widgets: Vec::new() }]) }), ); @@ -3221,7 +3224,10 @@ fn static_input_properties() -> InputProperties { Box::new(|node_id, index, context| { let (document_node, input_name, input_description) = node_properties::query_node_and_input_info(node_id, index, context)?; let (_, coherent_noise_active, _, _, _, _) = node_properties::query_noise_pattern_state(node_id, context)?; - let domain_warp_type = node_properties::domain_warp_type_widget(ParameterWidgetsInfo::new(document_node, node_id, index, input_name, input_description, true), !coherent_noise_active); + let domain_warp_type = enum_choice::() + .for_socket(ParameterWidgetsInfo::new(document_node, node_id, index, input_name, input_description, true)) + .disabled(!coherent_noise_active) + .property_row(); Ok(vec![domain_warp_type]) }), ); @@ -3242,7 +3248,10 @@ fn static_input_properties() -> InputProperties { Box::new(|node_id, index, context| { let (document_node, input_name, input_description) = node_properties::query_node_and_input_info(node_id, index, context)?; let (_, coherent_noise_active, _, _, _, _) = node_properties::query_noise_pattern_state(node_id, context)?; - let fractal_type_row = node_properties::fractal_type_widget(ParameterWidgetsInfo::new(document_node, node_id, index, input_name, input_description, true), !coherent_noise_active); + let fractal_type_row = enum_choice::() + .for_socket(ParameterWidgetsInfo::new(document_node, node_id, index, input_name, input_description, true)) + .disabled(!coherent_noise_active) + .property_row(); Ok(vec![fractal_type_row]) }), ); @@ -3333,10 +3342,10 @@ fn static_input_properties() -> InputProperties { Box::new(|node_id, index, context| { let (document_node, input_name, input_description) = node_properties::query_node_and_input_info(node_id, index, context)?; let (_, coherent_noise_active, cellular_noise_active, _, _, _) = node_properties::query_noise_pattern_state(node_id, context)?; - let cellular_distance_function_row = node_properties::cellular_distance_function_widget( - ParameterWidgetsInfo::new(document_node, node_id, index, input_name, input_description, true), - !coherent_noise_active || !cellular_noise_active, - ); + let cellular_distance_function_row = enum_choice::() + .for_socket(ParameterWidgetsInfo::new(document_node, node_id, index, input_name, input_description, true)) + .disabled(!coherent_noise_active || !cellular_noise_active) + .property_row(); Ok(vec![cellular_distance_function_row]) }), ); @@ -3345,10 +3354,10 @@ fn static_input_properties() -> InputProperties { Box::new(|node_id, index, context| { let (document_node, input_name, input_description) = node_properties::query_node_and_input_info(node_id, index, context)?; let (_, coherent_noise_active, cellular_noise_active, _, _, _) = node_properties::query_noise_pattern_state(node_id, context)?; - let cellular_return_type = node_properties::cellular_return_type_widget( - ParameterWidgetsInfo::new(document_node, node_id, index, input_name, input_description, true), - !coherent_noise_active || !cellular_noise_active, - ); + let cellular_return_type = enum_choice::() + .for_socket(ParameterWidgetsInfo::new(document_node, node_id, index, input_name, input_description, true)) + .disabled(!coherent_noise_active || !cellular_noise_active) + .property_row(); Ok(vec![cellular_return_type]) }), ); diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 876e0ea3f7..140172a623 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -5,6 +5,7 @@ use super::utility_types::FrontendGraphDataType; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::utility_types::network_interface::InputConnector; use crate::messages::prelude::*; +use choice::enum_choice; use dyn_any::DynAny; use glam::{DAffine2, DVec2, IVec2, UVec2}; use graph_craft::Type; @@ -16,7 +17,6 @@ use graphene_core::raster::{ BlendMode, CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute, SelectiveColorChoice, }; -use graphene_core::registry::ChoiceTypeStatic; use graphene_core::text::Font; use graphene_core::vector::generator_nodes::grid; use graphene_core::vector::misc::CentroidType; @@ -86,6 +86,10 @@ pub fn add_blank_assist(widgets: &mut Vec) { ]); } +pub fn start_widgets(parameter_widgets_info: ParameterWidgetsInfo, data_type: FrontendGraphDataType) -> Vec { + start_widgets_exposable(parameter_widgets_info, data_type, true) +} + pub fn start_widgets_exposable(parameter_widgets_info: ParameterWidgetsInfo, data_type: FrontendGraphDataType, exposable: bool) -> Vec { let ParameterWidgetsInfo { document_node, @@ -113,10 +117,6 @@ pub fn start_widgets_exposable(parameter_widgets_info: ParameterWidgetsInfo, dat widgets } -pub fn start_widgets(parameter_widgets_info: ParameterWidgetsInfo, data_type: FrontendGraphDataType) -> Vec { - start_widgets_exposable(parameter_widgets_info, data_type, true) -} - pub(crate) fn property_from_type( node_id: NodeId, index: usize, @@ -169,46 +169,67 @@ pub(crate) fn property_from_type( _ => { use std::any::TypeId; match concrete_type.id { - Some(x) if x == TypeId::of::() => bool_widget(default_info, CheckboxInput::default()).into(), + // =============== + // PRIMITIVE TYPES + // =============== Some(x) if x == TypeId::of::() => number_widget(default_info, number_input.min(min(f64::NEG_INFINITY)).max(max(f64::INFINITY))).into(), Some(x) if x == TypeId::of::() => number_widget(default_info, number_input.int().min(min(0.)).max(max(f64::from(u32::MAX)))).into(), Some(x) if x == TypeId::of::() => number_widget(default_info, number_input.int().min(min(0.))).into(), + Some(x) if x == TypeId::of::() => bool_widget(default_info, CheckboxInput::default()).into(), Some(x) if x == TypeId::of::() => text_widget(default_info).into(), - Some(x) if x == TypeId::of::() => color_widget(default_info, ColorInput::default().allow_none(false)), - Some(x) if x == TypeId::of::>() => color_widget(default_info, ColorInput::default().allow_none(true)), - Some(x) if x == TypeId::of::() => color_widget(default_info, ColorInput::default().allow_none(false)), Some(x) if x == TypeId::of::() => vector2_widget(default_info, "X", "Y", "", None), Some(x) if x == TypeId::of::() => vector2_widget(default_info, "X", "Y", "", Some(0.)), Some(x) if x == TypeId::of::() => vector2_widget(default_info, "X", "Y", "", None), + // ========================== + // PRIMITIVE COLLECTION TYPES + // ========================== Some(x) if x == TypeId::of::>() => array_of_number_widget(default_info, TextInput::default()).into(), Some(x) if x == TypeId::of::>() => array_of_vector2_widget(default_info, TextInput::default()).into(), - Some(x) if x == TypeId::of::() => font_widget(default_info).into(), - Some(x) if x == TypeId::of::() => curve_widget(default_info), + // ==================== + // GRAPHICAL DATA TYPES + // ==================== Some(x) if x == TypeId::of::() => vector_data_widget(default_info).into(), Some(x) if x == TypeId::of::() || x == TypeId::of::>() || x == TypeId::of::() => raster_widget(default_info).into(), Some(x) if x == TypeId::of::() => group_widget(default_info).into(), - Some(x) if x == TypeId::of::() => reference_point_widget(default_info, false).into(), + // ============ + // STRUCT TYPES + // ============ + Some(x) if x == TypeId::of::() => color_widget(default_info, ColorInput::default().allow_none(false)), + Some(x) if x == TypeId::of::>() => color_widget(default_info, ColorInput::default().allow_none(true)), + Some(x) if x == TypeId::of::() => color_widget(default_info, ColorInput::default().allow_none(false)), + Some(x) if x == TypeId::of::() => font_widget(default_info).into(), + Some(x) if x == TypeId::of::() => curve_widget(default_info), Some(x) if x == TypeId::of::() => footprint_widget(default_info, &mut extra_widgets), + // =============================== + // MANUALLY IMPLEMENTED ENUM TYPES + // =============================== + Some(x) if x == TypeId::of::() => reference_point_widget(default_info, false).into(), Some(x) if x == TypeId::of::() => blend_mode_widget(default_info), + // ========================= + // AUTO-GENERATED ENUM TYPES + // ========================= + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), - Some(x) if x == TypeId::of::() => fractal_type_widget(default_info, false), - Some(x) if x == TypeId::of::() => cellular_distance_function_widget(default_info, false), - Some(x) if x == TypeId::of::() => cellular_return_type_widget(default_info, false), - Some(x) if x == TypeId::of::() => domain_warp_type_widget(default_info, false), - Some(x) if x == TypeId::of::() => relative_absolute_widget(default_info), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).disabled(false).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).disabled(false).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).disabled(false).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).disabled(false).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).disabled(false).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), - Some(x) if x == TypeId::of::() => fill_type_widget(default_info), - Some(x) if x == TypeId::of::() => gradient_type_widget(default_info), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), + // ===== + // OTHER + // ===== _ => { let mut widgets = start_widgets(default_info, FrontendGraphDataType::General); widgets.extend_from_slice(&[ @@ -237,184 +258,6 @@ pub(crate) fn property_from_type( Ok(extra_widgets) } -pub mod choice { - use super::ParameterWidgetsInfo; - use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType; - use crate::messages::tool::tool_messages::tool_prelude::*; - use graph_craft::document::value::TaggedValue; - use graphene_core::registry::{ChoiceTypeStatic, ChoiceWidgetHint, VariantMetadata}; - use std::marker::PhantomData; - - pub trait WidgetFactory { - type Value: Clone + 'static; - - fn disabled(self, disabled: bool) -> Self; - fn build(&self, current: Self::Value, updater_factory: impl Fn() -> U, committer_factory: impl Fn() -> C) -> WidgetHolder - where - U: Fn(&Self::Value) -> Message + 'static + Send + Sync, - C: Fn(&()) -> Message + 'static + Send + Sync; - fn description(&self) -> Option<&str>; - } - - pub fn enum_choice() -> EnumChoice { - EnumChoice { - disabled: false, - phantom: PhantomData, - } - } - - pub struct EnumChoice { - disabled: bool, - phantom: PhantomData, - } - - impl EnumChoice { - pub fn for_socket(self, parameter_info: ParameterWidgetsInfo) -> ForSocket { - ForSocket { - widget_factory: self, - parameter_info, - exposable: true, - } - } - pub fn for_value(self, current: E) -> ForValue { - todo!() - } - pub fn disabled(self, disabled: bool) -> Self { - Self { disabled, ..self } - } - - pub fn into_menu_entries(self, action: impl Fn(E) -> Message + 'static + Send + Sync) -> Vec> { - todo!() - } - - fn dropdown(&self, current: E, updater_factory: impl Fn() -> U, committer_factory: impl Fn() -> C) -> WidgetHolder - where - U: Fn(&E) -> Message + 'static + Send + Sync, - C: Fn(&()) -> Message + 'static + Send + Sync, - { - let items = E::list() - .into_iter() - .map(|group| { - group - .into_iter() - .map(|(item, metadata)| { - let item = item.clone(); - let updater = updater_factory(); - let committer = committer_factory(); - MenuListEntry::new(metadata.name.as_ref()) - .label(metadata.label.as_ref()) - .on_update(move |_| updater(&item)) - .on_commit(committer) - }) - .collect() - }) - .collect(); - DropdownInput::new(items).disabled(self.disabled).selected_index(Some(current.as_u32())).widget_holder() - } - - fn radio_buttons(&self, current: E, updater_factory: impl Fn() -> U, committer_factory: impl Fn() -> C) -> WidgetHolder - where - U: Fn(&E) -> Message + 'static + Send + Sync, - C: Fn(&()) -> Message + 'static + Send + Sync, - { - let items = E::list() - .into_iter() - .map(|group| group.into_iter()) - .flatten() - .map(|(item, var_meta)| { - let item = item.clone(); - let updater = updater_factory(); - let committer = committer_factory(); - let red = RadioEntryData::new(var_meta.name.as_ref()).on_update(move |_| updater(&item)).on_commit(committer); - let red = match (var_meta.icon.as_deref(), var_meta.docstring.as_deref()) { - (None, None) => red.label(var_meta.label.as_ref()), - (None, Some(doc)) => red.label(var_meta.label.as_ref()).tooltip(doc), - (Some(icon), None) => red.icon(icon).tooltip(var_meta.label.as_ref()), - (Some(icon), Some(doc)) => red.icon(icon).tooltip(format!("{}\n\n{}", var_meta.label, doc)), - }; - red - }) - .collect(); - RadioInput::new(items).selected_index(Some(current.as_u32())).widget_holder() - } - } - impl WidgetFactory for EnumChoice { - type Value = E; - fn disabled(self, disabled: bool) -> Self { - Self { disabled, ..self } - } - - fn description(&self) -> Option<&str> { - E::DESCRIPTION - } - - fn build(&self, current: Self::Value, updater_factory: impl Fn() -> U, committer_factory: impl Fn() -> C) -> WidgetHolder - where - U: Fn(&Self::Value) -> Message + 'static + Send + Sync, - C: Fn(&()) -> Message + 'static + Send + Sync, - { - match E::WIDGET_HINT { - ChoiceWidgetHint::Dropdown => self.dropdown(current, updater_factory, committer_factory), - ChoiceWidgetHint::RadioButtons => self.radio_buttons(current, updater_factory, committer_factory), - } - } - } - - pub struct ForSocket<'p, W> { - widget_factory: W, - parameter_info: ParameterWidgetsInfo<'p>, - exposable: bool, - } - - impl<'p, W> ForSocket<'p, W> - where - W: WidgetFactory, - for<'a> &'a W::Value: TryFrom<&'a TaggedValue>, - W::Value: Clone, - TaggedValue: From, - { - pub fn disabled(self, disabled: bool) -> Self { - Self { - widget_factory: self.widget_factory.disabled(disabled), - ..self - } - } - - pub fn exposable(self, exposable: bool) -> Self { - Self { exposable, ..self } - } - - pub fn property_row(self) -> LayoutGroup { - let ParameterWidgetsInfo { document_node, node_id, index, .. } = self.parameter_info; - - let mut widgets = super::start_widgets_exposable(self.parameter_info, FrontendGraphDataType::General, self.exposable); - - let Some(input) = document_node.inputs.get(index) else { - log::warn!("A widget failed to be built because its node's input index is invalid."); - return LayoutGroup::Row { widgets: vec![] }; - }; - - let input: Option = input.as_non_exposed_value().and_then(|v| <&W::Value as TryFrom<&TaggedValue>>::try_from(v).ok()).map(Clone::clone); - - if let Some(current) = input { - let committer = || super::commit_value; - let updater = || super::update_value(move |v: &W::Value| TaggedValue::from(v.clone()), node_id, index); - let widget = self.widget_factory.build(current, updater, committer); - widgets.extend_from_slice(&[Separator::new(SeparatorType::Unrelated).widget_holder(), widget]); - } - let mut row = LayoutGroup::Row { widgets }; - if let Some(desc) = self.widget_factory.description() { - row = row.with_tooltip(desc); - } - row - } - } - - pub struct ForValue(PhantomData); -} - -use choice::enum_choice; - pub fn text_widget(parameter_widgets_info: ParameterWidgetsInfo) -> Vec { let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; @@ -953,49 +796,7 @@ pub fn number_widget(parameter_widgets_info: ParameterWidgetsInfo, number_props: widgets } -pub fn noise_type_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup { - enum_choice::().for_socket(parameter_widgets_info).property_row() -} - -// TODO: Generalize this instead of using a separate function per dropdown menu enum -pub fn fractal_type_widget(parameter_widgets_info: ParameterWidgetsInfo, disabled: bool) -> LayoutGroup { - enum_choice::().for_socket(parameter_widgets_info).disabled(disabled).property_row() -} - -// TODO: Generalize this instead of using a separate function per dropdown menu enum -pub fn cellular_distance_function_widget(parameter_widgets_info: ParameterWidgetsInfo, disabled: bool) -> LayoutGroup { - enum_choice::().for_socket(parameter_widgets_info).disabled(disabled).property_row() -} - -// TODO: Generalize this instead of using a separate function per dropdown menu enum -pub fn cellular_return_type_widget(parameter_widgets_info: ParameterWidgetsInfo, disabled: bool) -> LayoutGroup { - enum_choice::().for_socket(parameter_widgets_info).disabled(disabled).property_row() -} - -// TODO: Generalize this instead of using a separate function per dropdown menu enum -pub fn domain_warp_type_widget(parameter_widgets_info: ParameterWidgetsInfo, disabled: bool) -> LayoutGroup { - enum_choice::().for_socket(parameter_widgets_info).disabled(disabled).property_row() -} - -// TODO: Generalize this instead of using a separate function per dropdown menu enum -pub fn relative_absolute_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup { - let ParameterWidgetsInfo { node_id, index, .. } = parameter_widgets_info; - - vec![ - DropdownInput::new(vec![vec![ - MenuListEntry::new("Relative") - .label("Relative") - .on_update(update_value(|_| TaggedValue::RelativeAbsolute(RelativeAbsolute::Relative), node_id, index)), - MenuListEntry::new("Absolute") - .label("Absolute") - .on_update(update_value(|_| TaggedValue::RelativeAbsolute(RelativeAbsolute::Absolute), node_id, index)), - ]]) - .widget_holder(), - ] - .into() -} - -// TODO: Generalize this instead of using a separate function per dropdown menu enum +// TODO: Auto-generate this enum dropdown menu widget pub fn blend_mode_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup { let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; @@ -1030,40 +831,6 @@ pub fn blend_mode_widget(parameter_widgets_info: ParameterWidgetsInfo) -> Layout LayoutGroup::Row { widgets }.with_tooltip("Formula used for blending") } -pub fn fill_type_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup { - let ParameterWidgetsInfo { node_id, index, .. } = parameter_widgets_info; - - vec![ - DropdownInput::new(vec![vec![ - MenuListEntry::new("Solid") - .label("Solid") - .on_update(update_value(|_| TaggedValue::FillType(FillType::Solid), node_id, index)), - MenuListEntry::new("Gradient") - .label("Gradient") - .on_update(update_value(|_| TaggedValue::FillType(FillType::Gradient), node_id, index)), - ]]) - .widget_holder(), - ] - .into() -} - -pub fn gradient_type_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup { - let ParameterWidgetsInfo { node_id, index, .. } = parameter_widgets_info; - - vec![ - DropdownInput::new(vec![vec![ - MenuListEntry::new("Linear") - .label("Linear") - .on_update(update_value(|_| TaggedValue::GradientType(GradientType::Linear), node_id, index)), - MenuListEntry::new("Radial") - .label("Radial") - .on_update(update_value(|_| TaggedValue::GradientType(GradientType::Radial), node_id, index)), - ]]) - .widget_holder(), - ] - .into() -} - pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button: ColorInput) -> LayoutGroup { let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; @@ -1211,40 +978,19 @@ pub(crate) fn channel_mixer_properties(node_id: NodeId, context: &mut NodeProper // Monochrome let monochrome_index = 1; - let monochrome = bool_widget(ParameterWidgetsInfo::from_index(document_node, node_id, monochrome_index, true, context), CheckboxInput::default()); - let is_monochrome = match document_node.inputs[monochrome_index].as_value() { + let is_monochrome = bool_widget(ParameterWidgetsInfo::from_index(document_node, node_id, monochrome_index, true, context), CheckboxInput::default()); + let is_monochrome_value = match document_node.inputs[monochrome_index].as_value() { Some(TaggedValue::Bool(monochrome_choice)) => *monochrome_choice, _ => false, }; // Output channel choice let output_channel_index = 18; - let mut output_channel = vec![TextLabel::new("Output Channel").widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder()]; - add_blank_assist(&mut output_channel); - - let Some(input) = document_node.inputs.get(output_channel_index) else { - log::warn!("A widget failed to be built because its node's input index is invalid."); - return vec![]; - }; - if let Some(&TaggedValue::RedGreenBlue(choice)) = input.as_non_exposed_value() { - let entries = vec![ - RadioEntryData::new(format!("{:?}", RedGreenBlue::Red)) - .label(RedGreenBlue::Red.to_string()) - .on_update(update_value(|_| TaggedValue::RedGreenBlue(RedGreenBlue::Red), node_id, output_channel_index)) - .on_commit(commit_value), - RadioEntryData::new(format!("{:?}", RedGreenBlue::Green)) - .label(RedGreenBlue::Green.to_string()) - .on_update(update_value(|_| TaggedValue::RedGreenBlue(RedGreenBlue::Green), node_id, output_channel_index)) - .on_commit(commit_value), - RadioEntryData::new(format!("{:?}", RedGreenBlue::Blue)) - .label(RedGreenBlue::Blue.to_string()) - .on_update(update_value(|_| TaggedValue::RedGreenBlue(RedGreenBlue::Blue), node_id, output_channel_index)) - .on_commit(commit_value), - ]; - output_channel.extend([RadioInput::new(entries).selected_index(Some(choice as u32)).widget_holder()]); - }; - - let is_output_channel = match &document_node.inputs[output_channel_index].as_value() { + let output_channel = enum_choice::() + .for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, output_channel_index, true, context)) + .exposable(false) + .property_row(); + let output_channel_value = match &document_node.inputs[output_channel_index].as_value() { Some(TaggedValue::RedGreenBlue(choice)) => choice, _ => { warn!("Channel Mixer node properties panel could not be displayed."); @@ -1253,7 +999,7 @@ pub(crate) fn channel_mixer_properties(node_id: NodeId, context: &mut NodeProper }; // Output Channel modes - let (red_output_index, green_output_index, blue_output_index, constant_output_index) = match (is_monochrome, is_output_channel) { + let (red_output_index, green_output_index, blue_output_index, constant_output_index) = match (is_monochrome_value, output_channel_value) { (true, _) => (2, 3, 4, 5), (false, RedGreenBlue::Red) => (6, 7, 8, 9), (false, RedGreenBlue::Green) => (10, 11, 12, 13), @@ -1266,11 +1012,11 @@ pub(crate) fn channel_mixer_properties(node_id: NodeId, context: &mut NodeProper let constant = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, constant_output_index, true, context), number_input); // Monochrome - let mut layout = vec![LayoutGroup::Row { widgets: monochrome }]; + let mut layout = vec![LayoutGroup::Row { widgets: is_monochrome }]; // Output channel choice - if !is_monochrome { - layout.push(LayoutGroup::Row { widgets: output_channel }); - }; + if !is_monochrome_value { + layout.push(output_channel); + } // Channel values layout.extend([ LayoutGroup::Row { widgets: red }, @@ -1291,19 +1037,9 @@ pub(crate) fn selective_color_properties(node_id: NodeId, context: &mut NodeProp }; // Colors choice let colors_index = 38; - /*let mut colors = vec![TextLabel::new("Colors").widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder()]; - add_blank_assist(&mut colors); - - let Some(input) = document_node.inputs.get(colors_index) else { - log::warn!("A widget failed to be built because its node's input index is invalid."); - return vec![]; - }; - if let Some(&TaggedValue::SelectiveColorChoice(choice)) = input.as_non_exposed_value() { - colors.extend([radio_buttons(enum_source::(), node_id, colors_index, choice).widget_holder()]); - }*/ - let colours = enum_choice::() - .for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, colors_index, false, context)) + let colors = enum_choice::() + .for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, colors_index, true, context)) .exposable(false) .property_row(); @@ -1335,38 +1071,20 @@ pub(crate) fn selective_color_properties(node_id: NodeId, context: &mut NodeProp // Mode let mode_index = 1; - let mut mode = start_widgets(ParameterWidgetsInfo::from_index(document_node, node_id, mode_index, true, context), FrontendGraphDataType::General); - mode.push(Separator::new(SeparatorType::Unrelated).widget_holder()); - - let Some(input) = document_node.inputs.get(mode_index) else { - log::warn!("A widget failed to be built because its node's input index is invalid."); - return vec![]; - }; - if let Some(&TaggedValue::RelativeAbsolute(relative_or_absolute)) = input.as_non_exposed_value() { - let entries = vec![ - RadioEntryData::new("relative") - .label("Relative") - .on_update(update_value(|_| TaggedValue::RelativeAbsolute(RelativeAbsolute::Relative), node_id, mode_index)) - .on_commit(commit_value), - RadioEntryData::new("absolute") - .label("Absolute") - .on_update(update_value(|_| TaggedValue::RelativeAbsolute(RelativeAbsolute::Absolute), node_id, mode_index)) - .on_commit(commit_value), - ]; - mode.push(RadioInput::new(entries).selected_index(Some(relative_or_absolute as u32)).widget_holder()); - }; + let mode = enum_choice::() + .for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, mode_index, true, context)) + .property_row(); vec![ // Colors choice - //LayoutGroup::Row { widgets: colors }, - colours, + colors, // CMYK LayoutGroup::Row { widgets: cyan }, LayoutGroup::Row { widgets: magenta }, LayoutGroup::Row { widgets: yellow }, LayoutGroup::Row { widgets: black }, // Mode - LayoutGroup::Row { widgets: mode }, + mode, ] } @@ -1839,7 +1557,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte let new_gradient2 = gradient.clone(); let entries = vec![ - RadioEntryData::new("linear") + RadioEntryData::new("Linear") .label("Linear") .on_update(update_value( move |_| { @@ -1851,7 +1569,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte fill_index, )) .on_commit(commit_value), - RadioEntryData::new("radial") + RadioEntryData::new("Radial") .label("Radial") .on_update(update_value( move |_| { @@ -2058,3 +1776,187 @@ impl<'a> ParameterWidgetsInfo<'a> { } } } + +pub mod choice { + use super::ParameterWidgetsInfo; + use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType; + use crate::messages::tool::tool_messages::tool_prelude::*; + use graph_craft::document::value::TaggedValue; + use graphene_core::registry::{ChoiceTypeStatic, ChoiceWidgetHint}; + use std::marker::PhantomData; + + pub trait WidgetFactory { + type Value: Clone + 'static; + + fn disabled(self, disabled: bool) -> Self; + + fn build(&self, current: Self::Value, updater_factory: impl Fn() -> U, committer_factory: impl Fn() -> C) -> WidgetHolder + where + U: Fn(&Self::Value) -> Message + 'static + Send + Sync, + C: Fn(&()) -> Message + 'static + Send + Sync; + + fn description(&self) -> Option<&str>; + } + + pub fn enum_choice() -> EnumChoice { + EnumChoice { + disabled: false, + phantom: PhantomData, + } + } + + pub struct EnumChoice { + disabled: bool, + phantom: PhantomData, + } + + impl EnumChoice { + pub fn for_socket(self, parameter_info: ParameterWidgetsInfo) -> ForSocket { + ForSocket { + widget_factory: self, + parameter_info, + exposable: true, + } + } + + /// Not yet implemented! + pub fn for_value(self, _current: E) -> ForValue { + todo!() + } + + pub fn disabled(self, disabled: bool) -> Self { + Self { disabled, ..self } + } + + /// Not yet implemented! + pub fn into_menu_entries(self, _action: impl Fn(E) -> Message + 'static + Send + Sync) -> Vec> { + todo!() + } + + fn dropdown_menu(&self, current: E, updater_factory: impl Fn() -> U, committer_factory: impl Fn() -> C) -> WidgetHolder + where + U: Fn(&E) -> Message + 'static + Send + Sync, + C: Fn(&()) -> Message + 'static + Send + Sync, + { + let items = E::list() + .into_iter() + .map(|group| { + group + .into_iter() + .map(|(item, metadata)| { + let item = item.clone(); + let updater = updater_factory(); + let committer = committer_factory(); + MenuListEntry::new(metadata.name.as_ref()) + .label(metadata.label.as_ref()) + .on_update(move |_| updater(&item)) + .on_commit(committer) + }) + .collect() + }) + .collect(); + DropdownInput::new(items).disabled(self.disabled).selected_index(Some(current.as_u32())).widget_holder() + } + + fn radio_buttons(&self, current: E, updater_factory: impl Fn() -> U, committer_factory: impl Fn() -> C) -> WidgetHolder + where + U: Fn(&E) -> Message + 'static + Send + Sync, + C: Fn(&()) -> Message + 'static + Send + Sync, + { + let items = E::list() + .into_iter() + .map(|group| group.into_iter()) + .flatten() + .map(|(item, var_meta)| { + let item = item.clone(); + let updater = updater_factory(); + let committer = committer_factory(); + let entry = RadioEntryData::new(var_meta.name.as_ref()).on_update(move |_| updater(&item)).on_commit(committer); + match (var_meta.icon.as_deref(), var_meta.docstring.as_deref()) { + (None, None) => entry.label(var_meta.label.as_ref()), + (None, Some(doc)) => entry.label(var_meta.label.as_ref()).tooltip(doc), + (Some(icon), None) => entry.icon(icon).tooltip(var_meta.label.as_ref()), + (Some(icon), Some(doc)) => entry.icon(icon).tooltip(format!("{}\n\n{}", var_meta.label, doc)), + } + }) + .collect(); + RadioInput::new(items).selected_index(Some(current.as_u32())).widget_holder() + } + } + + impl WidgetFactory for EnumChoice { + type Value = E; + + fn disabled(self, disabled: bool) -> Self { + Self { disabled, ..self } + } + + fn description(&self) -> Option<&str> { + E::DESCRIPTION + } + + fn build(&self, current: Self::Value, updater_factory: impl Fn() -> U, committer_factory: impl Fn() -> C) -> WidgetHolder + where + U: Fn(&Self::Value) -> Message + 'static + Send + Sync, + C: Fn(&()) -> Message + 'static + Send + Sync, + { + match E::WIDGET_HINT { + ChoiceWidgetHint::Dropdown => self.dropdown_menu(current, updater_factory, committer_factory), + ChoiceWidgetHint::RadioButtons => self.radio_buttons(current, updater_factory, committer_factory), + } + } + } + + pub struct ForSocket<'p, W> { + widget_factory: W, + parameter_info: ParameterWidgetsInfo<'p>, + exposable: bool, + } + + impl<'p, W> ForSocket<'p, W> + where + W: WidgetFactory, + W::Value: Clone, + for<'a> &'a W::Value: TryFrom<&'a TaggedValue>, + TaggedValue: From, + { + pub fn disabled(self, disabled: bool) -> Self { + Self { + widget_factory: self.widget_factory.disabled(disabled), + ..self + } + } + + pub fn exposable(self, exposable: bool) -> Self { + Self { exposable, ..self } + } + + pub fn property_row(self) -> LayoutGroup { + let ParameterWidgetsInfo { document_node, node_id, index, .. } = self.parameter_info; + + let mut widgets = super::start_widgets_exposable(self.parameter_info, FrontendGraphDataType::General, self.exposable); + + let Some(input) = document_node.inputs.get(index) else { + log::warn!("A widget failed to be built because its node's input index is invalid."); + return LayoutGroup::Row { widgets: vec![] }; + }; + + let input: Option = input.as_non_exposed_value().and_then(|v| <&W::Value as TryFrom<&TaggedValue>>::try_from(v).ok()).map(Clone::clone); + + if let Some(current) = input { + let committer = || super::commit_value; + let updater = || super::update_value(move |v: &W::Value| TaggedValue::from(v.clone()), node_id, index); + let widget = self.widget_factory.build(current, updater, committer); + widgets.extend_from_slice(&[Separator::new(SeparatorType::Unrelated).widget_holder(), widget]); + } + + let mut row = LayoutGroup::Row { widgets }; + if let Some(desc) = self.widget_factory.description() { + row = row.with_tooltip(desc); + } + row + } + } + + pub struct ForValue(PhantomData); +} diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 8080ac9dcb..12b8f17b30 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -90,11 +90,11 @@ impl<'a> MessageHandler> for Gradien impl LayoutHolder for GradientTool { fn layout(&self) -> Layout { let gradient_type = RadioInput::new(vec![ - RadioEntryData::new("linear") + RadioEntryData::new("Linear") .label("Linear") .tooltip("Linear gradient") .on_update(move |_| GradientToolMessage::UpdateOptions(GradientOptionsUpdate::Type(GradientType::Linear)).into()), - RadioEntryData::new("radial") + RadioEntryData::new("Radial") .label("Radial") .tooltip("Radial gradient") .on_update(move |_| GradientToolMessage::UpdateOptions(GradientOptionsUpdate::Type(GradientType::Radial)).into()), @@ -611,7 +611,7 @@ mod test_gradient { let (gradient, transform) = get_gradient(&mut editor).await; - // Gradient goes from secondary colour to primary colour + // Gradient goes from secondary color to primary color let stops = gradient.stops.iter().map(|stop| (stop.0, stop.1.to_rgba8_srgb())).collect::>(); assert_eq!(stops, vec![(0., Color::BLUE.to_rgba8_srgb()), (1., Color::GREEN.to_rgba8_srgb())]); assert!(transform.transform_point2(gradient.start).abs_diff_eq(DVec2::new(2., 3.), 1e-10)); diff --git a/node-graph/gcore/src/animation.rs b/node-graph/gcore/src/animation.rs index 8bbdc8b4de..97e20dc6a2 100644 --- a/node-graph/gcore/src/animation.rs +++ b/node-graph/gcore/src/animation.rs @@ -4,7 +4,6 @@ const DAY: f64 = 1000. * 3600. * 24.; #[derive(Debug, Clone, Copy, PartialEq, Eq, dyn_any::DynAny, Default, Hash, node_macro::ChoiceType)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -/// Real Time Mode pub enum RealTimeMode { #[label("UTC")] Utc, diff --git a/node-graph/gcore/src/ops.rs b/node-graph/gcore/src/ops.rs index 44d2bc5f6c..4c83a67f63 100644 --- a/node-graph/gcore/src/ops.rs +++ b/node-graph/gcore/src/ops.rs @@ -533,11 +533,11 @@ fn extract_xy>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2 } } +/// The X or Y component of a vector2. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] #[widget(Dropdown)] -/// X or Y Component of Vector2 pub enum XY { #[default] X, diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index c8b7bcf8cc..0aaa25f6a0 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -38,6 +38,7 @@ use spirv_std::num_traits::float::Float; #[widget(Dropdown)] pub enum LuminanceCalculation { #[default] + #[label("sRGB")] SRGB, Perceptual, AverageChannels, @@ -821,10 +822,11 @@ async fn vibrance>( image } +/// Color Channel #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] -/// Color Channel +#[widget(Radio)] pub enum RedGreenBlue { #[default] Red, @@ -832,10 +834,11 @@ pub enum RedGreenBlue { Blue, } +/// Color Channel #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] -/// Color Channel +#[widget(Radio)] pub enum RedGreenBlueAlpha { #[default] Red, @@ -844,21 +847,18 @@ pub enum RedGreenBlueAlpha { Alpha, } +/// Style of noise pattern #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] #[widget(Dropdown)] -/// Style of noise pattern pub enum NoiseType { #[default] Perlin, - #[label("OpenSimplex2")] OpenSimplex2, - #[label("OpenSimplex2S")] OpenSimplex2S, - Cellular, ValueCubic, Value, @@ -882,10 +882,10 @@ pub enum FractalType { DomainWarpIndependent, } +/// Distance function used by the cellular noise #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] -/// Distance function used by the cellular noise pub enum CellularDistanceFunction { #[default] Euclidean, @@ -915,21 +915,18 @@ pub enum CellularReturnType { Division, } +/// Type of domain warp #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] #[widget(Dropdown)] -/// Type of domain warp pub enum DomainWarpType { #[default] None, - #[label("OpenSimplex2")] OpenSimplex2, - #[label("OpenSimplex2 Reduced")] OpenSimplex2Reduced, - BasicGrid, } @@ -1032,22 +1029,14 @@ async fn channel_mixer>( #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[widget(Radio)] pub enum RelativeAbsolute { #[default] Relative, Absolute, } -impl core::fmt::Display for RelativeAbsolute { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - RelativeAbsolute::Relative => write!(f, "Relative"), - RelativeAbsolute::Absolute => write!(f, "Absolute"), - } - } -} - #[repr(C)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] diff --git a/node-graph/gcore/src/registry.rs b/node-graph/gcore/src/registry.rs index 95b12c6547..f77c3602c3 100644 --- a/node-graph/gcore/src/registry.rs +++ b/node-graph/gcore/src/registry.rs @@ -65,19 +65,19 @@ pub enum ChoiceWidgetHint { RadioButtons, } -// Translation struct between macro and definition +/// Translation struct between macro and definition. #[derive(Clone, Debug)] pub struct VariantMetadata { - /// Name as declared in source code + /// Name as declared in source code. pub name: Cow<'static, str>, - /// Name to be displayed in UI + /// Name to be displayed in UI. pub label: Cow<'static, str>, - /// User-facing documentation text + /// User-facing documentation text. pub docstring: Option>, - /// Name of icon to display in radio buttons and such + /// Name of icon to display in radio buttons and such. pub icon: Option>, } diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index 00582ea49a..8e275c97df 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -19,17 +19,12 @@ pub enum BooleanOperation { #[default] #[icon("BooleanUnion")] Union, - #[icon("BooleanSubtractFront")] SubtractFront, - - /// Output shape contains points which are contained by the front shape but *not* by the back shape. #[icon("BooleanSubtractBack")] SubtractBack, - #[icon("BooleanIntersect")] Intersect, - #[icon("BooleanDifference")] Difference, } @@ -39,7 +34,7 @@ pub trait AsU32 { } impl AsU32 for u32 { fn as_u32(&self) -> u32 { - *self as u32 + *self } } diff --git a/node-graph/gcore/src/vector/style.rs b/node-graph/gcore/src/vector/style.rs index 86ca121d33..ffb307749e 100644 --- a/node-graph/gcore/src/vector/style.rs +++ b/node-graph/gcore/src/vector/style.rs @@ -7,7 +7,8 @@ use dyn_any::DynAny; use glam::{DAffine2, DVec2}; use std::fmt::Write; -#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, specta::Type)] +#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, specta::Type, node_macro::ChoiceType)] +#[widget(Radio)] pub enum GradientType { #[default] Linear, @@ -462,7 +463,8 @@ impl From for FillChoice { /// Enum describing the type of [Fill]. #[repr(C)] -#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)] +#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type, node_macro::ChoiceType)] +#[widget(Radio)] pub enum FillType { #[default] Solid, diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index db19e76135..b851392e94 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -157,56 +157,75 @@ macro_rules! tagged_value { } tagged_value! { - // TODO: Eventually remove this migration document upgrade code - #[cfg_attr(all(feature = "serde", target_arch = "wasm32"), serde(deserialize_with = "graphene_core::raster::image::migrate_image_frame"))] - ImageFrame(graphene_core::raster::image::ImageFrameTable), - // TODO: Eventually remove this migration document upgrade code - #[cfg_attr(all(feature = "serde", target_arch = "wasm32"), serde(deserialize_with = "graphene_core::vector::migrate_vector_data"))] - VectorData(graphene_core::vector::VectorDataTable), - // TODO: Eventually remove this migration document upgrade code - #[cfg_attr(all(feature = "serde", target_arch = "wasm32"), serde(deserialize_with = "graphene_core::migrate_graphic_group"))] - GraphicGroup(graphene_core::GraphicGroupTable), - // TODO: Eventually remove this migration document upgrade code - #[cfg_attr(all(feature = "serde", target_arch = "wasm32"), serde(deserialize_with = "graphene_core::migrate_artboard_group"))] - ArtboardGroup(graphene_core::ArtboardGroupTable), - GraphicElement(graphene_core::GraphicElement), - Artboard(graphene_core::Artboard), - String(String), + // =============== + // PRIMITIVE TYPES + // =============== + #[cfg_attr(feature = "serde", serde(alias = "F32"))] // TODO: Eventually remove this alias document upgrade code + F64(f64), U32(u32), U64(u64), - // TODO: Eventually remove this alias document upgrade code - #[cfg_attr(feature = "serde", serde(alias = "F32"))] - F64(f64), - OptionalF64(Option), Bool(bool), + String(String), UVec2(UVec2), IVec2(IVec2), DVec2(DVec2), - OptionalDVec2(Option), DAffine2(DAffine2), + OptionalF64(Option), + OptionalDVec2(Option), + // ========================== + // PRIMITIVE COLLECTION TYPES + // ========================== + #[cfg_attr(feature = "serde", serde(alias = "VecF32"))] // TODO: Eventually remove this alias document upgrade code + VecF64(Vec), + VecU64(Vec), + VecDVec2(Vec), + F64Array4([f64; 4]), + NodePath(Vec), + #[cfg_attr(feature = "serde", serde(alias = "ManipulatorGroupIds"))] // TODO: Eventually remove this alias document upgrade code + PointIds(Vec), + // ==================== + // GRAPHICAL DATA TYPES + // ==================== + GraphicElement(graphene_core::GraphicElement), + #[cfg_attr(all(feature = "serde", target_arch = "wasm32"), serde(deserialize_with = "graphene_core::vector::migrate_vector_data"))] // TODO: Eventually remove this migration document upgrade code + VectorData(graphene_core::vector::VectorDataTable), + #[cfg_attr(all(feature = "serde", target_arch = "wasm32"), serde(deserialize_with = "graphene_core::raster::image::migrate_image_frame"))] // TODO: Eventually remove this migration document upgrade code + ImageFrame(graphene_core::raster::image::ImageFrameTable), + #[cfg_attr(all(feature = "serde", target_arch = "wasm32"), serde(deserialize_with = "graphene_core::migrate_graphic_group"))] // TODO: Eventually remove this migration document upgrade code + GraphicGroup(graphene_core::GraphicGroupTable), + #[cfg_attr(all(feature = "serde", target_arch = "wasm32"), serde(deserialize_with = "graphene_core::migrate_artboard_group"))] // TODO: Eventually remove this migration document upgrade code + ArtboardGroup(graphene_core::ArtboardGroupTable), + // ============ + // STRUCT TYPES + // ============ + Artboard(graphene_core::Artboard), Image(graphene_core::raster::Image), Color(graphene_core::raster::color::Color), OptionalColor(Option), + Palette(Vec), Subpaths(Vec>), - BlendMode(BlendMode), - LuminanceCalculation(LuminanceCalculation), - // ImaginateCache(ImaginateCache), - // ImaginateSamplingMethod(ImaginateSamplingMethod), - // ImaginateMaskStartingFill(ImaginateMaskStartingFill), - // ImaginateController(ImaginateController), Fill(graphene_core::vector::style::Fill), Stroke(graphene_core::vector::style::Stroke), - F64Array4([f64; 4]), - // TODO: Eventually remove this alias document upgrade code - #[cfg_attr(feature = "serde", serde(alias = "VecF32"))] - VecF64(Vec), - VecU64(Vec), - NodePath(Vec), - VecDVec2(Vec), + Gradient(graphene_core::vector::style::Gradient), + #[cfg_attr(feature = "serde", serde(alias = "GradientPositions"))] // TODO: Eventually remove this alias document upgrade code + GradientStops(graphene_core::vector::style::GradientStops), + Font(graphene_core::text::Font), + BrushStrokes(Vec), + BrushCache(BrushCache), + DocumentNode(DocumentNode), + Curve(graphene_core::raster::curve::Curve), + Footprint(graphene_core::transform::Footprint), + VectorModification(Box), + FontCache(Arc), + // ========== + // ENUM TYPES + // ========== + BlendMode(BlendMode), + LuminanceCalculation(LuminanceCalculation), XY(graphene_core::ops::XY), RedGreenBlue(graphene_core::raster::RedGreenBlue), - RealTimeMode(graphene_core::animation::RealTimeMode), RedGreenBlueAlpha(graphene_core::raster::RedGreenBlueAlpha), + RealTimeMode(graphene_core::animation::RealTimeMode), NoiseType(graphene_core::raster::NoiseType), FractalType(graphene_core::raster::FractalType), CellularDistanceFunction(graphene_core::raster::CellularDistanceFunction), @@ -220,26 +239,15 @@ tagged_value! { LineJoin(graphene_core::vector::style::LineJoin), FillType(graphene_core::vector::style::FillType), FillChoice(graphene_core::vector::style::FillChoice), - Gradient(graphene_core::vector::style::Gradient), GradientType(graphene_core::vector::style::GradientType), - // TODO: Eventually remove this alias document upgrade code - #[cfg_attr(feature = "serde", serde(alias = "GradientPositions"))] - GradientStops(graphene_core::vector::style::GradientStops), - // TODO: Eventually remove this alias document upgrade code - #[cfg_attr(feature = "serde", serde(alias = "ManipulatorGroupIds"))] - PointIds(Vec), - Font(graphene_core::text::Font), - BrushStrokes(Vec), - BrushCache(BrushCache), - DocumentNode(DocumentNode), - Curve(graphene_core::raster::curve::Curve), - Footprint(graphene_core::transform::Footprint), ReferencePoint(graphene_core::transform::ReferencePoint), - Palette(Vec), - VectorModification(Box), CentroidType(graphene_core::vector::misc::CentroidType), BooleanOperation(graphene_core::vector::misc::BooleanOperation), - FontCache(Arc), + + // ImaginateCache(ImaginateCache), + // ImaginateSamplingMethod(ImaginateSamplingMethod), + // ImaginateMaskStartingFill(ImaginateMaskStartingFill), + // ImaginateController(ImaginateController), } impl TaggedValue { diff --git a/node-graph/gstd/src/raster.rs b/node-graph/gstd/src/raster.rs index 7ece1c93d2..a8f42007a9 100644 --- a/node-graph/gstd/src/raster.rs +++ b/node-graph/gstd/src/raster.rs @@ -147,6 +147,7 @@ where image } + #[node_macro::node] fn combine_channels< // _P is the color of the input image. diff --git a/node-graph/node-macro/src/derive_choice_type.rs b/node-graph/node-macro/src/derive_choice_type.rs index dc9cbed8ac..86278c6622 100644 --- a/node-graph/node-macro/src/derive_choice_type.rs +++ b/node-graph/node-macro/src/derive_choice_type.rs @@ -7,7 +7,7 @@ pub fn derive_choice_type_impl(input_item: TokenStream) -> syn::Result(input_item).unwrap(); match input.data { - syn::Data::Enum(en) => derive_enum(&input.attrs, input.ident, en), + syn::Data::Enum(data_enum) => derive_enum(&input.attrs, input.ident, data_enum), _ => Err(syn::Error::new(input.ident.span(), "Only enums are supported at the moment")), } } @@ -23,13 +23,13 @@ enum WidgetHint { } impl Parse for WidgetHint { fn parse(input: syn::parse::ParseStream) -> syn::Result { - let tok: Ident = input.parse()?; - if tok == "Radio" { + let tokens: Ident = input.parse()?; + if tokens == "Radio" { Ok(Self::Radio) - } else if tok == "Dropdown" { + } else if tokens == "Dropdown" { Ok(Self::Dropdown) } else { - Err(syn::Error::new_spanned(tok, "Widget must be either Radio or Dropdown")) + Err(syn::Error::new_spanned(tokens, "Widget must be either Radio or Dropdown")) } } } @@ -41,20 +41,20 @@ struct BasicItem { icon: Option, } impl BasicItem { - fn read_attr(&mut self, attr: &Attribute) -> syn::Result<()> { - if attr.path().is_ident("label") { - let tok: LitStr = attr.parse_args()?; - self.label = tok.value(); + fn read_attribute(&mut self, attribute: &Attribute) -> syn::Result<()> { + if attribute.path().is_ident("label") { + let token: LitStr = attribute.parse_args()?; + self.label = token.value(); } - if attr.path().is_ident("icon") { - let tok: LitStr = attr.parse_args()?; - self.icon = Some(tok.value()); + if attribute.path().is_ident("icon") { + let token: LitStr = attribute.parse_args()?; + self.icon = Some(token.value()); } - if attr.path().is_ident("doc") { - if let Meta::NameValue(nv) = &attr.meta { - if let Expr::Lit(el) = &nv.value { - if let syn::Lit::Str(tok) = &el.lit { - self.description = Some(tok.value()); + if attribute.path().is_ident("doc") { + if let Meta::NameValue(meta_name_value) = &attribute.meta { + if let Expr::Lit(el) = &meta_name_value.value { + if let syn::Lit::Str(token) = &el.lit { + self.description = Some(token.value()); } } } @@ -68,44 +68,46 @@ struct Variant { basic_item: BasicItem, } -fn derive_enum(enum_attrs: &[Attribute], name: Ident, input: syn::DataEnum) -> syn::Result { +fn derive_enum(enum_attributes: &[Attribute], name: Ident, input: syn::DataEnum) -> syn::Result { let mut enum_info = Type { basic_item: BasicItem::default(), widget_hint: WidgetHint::Dropdown, }; - for att in enum_attrs { - enum_info.basic_item.read_attr(att)?; - if att.path().is_ident("widget") { - enum_info.widget_hint = att.parse_args()?; + for attribute in enum_attributes { + enum_info.basic_item.read_attribute(attribute)?; + if attribute.path().is_ident("widget") { + enum_info.widget_hint = attribute.parse_args()?; } } let mut variants = vec![Vec::new()]; - for va in &input.variants { + for variant in &input.variants { let mut basic_item = BasicItem::default(); - for attr in &va.attrs { - if attr.path().is_ident("menu_separator") { - attr.meta.require_path_only()?; + for attribute in &variant.attrs { + if attribute.path().is_ident("menu_separator") { + attribute.meta.require_path_only()?; variants.push(Vec::new()); } - basic_item.read_attr(attr)?; + basic_item.read_attribute(attribute)?; } - if basic_item.label.len() == 0 { - basic_item.label = ident_to_label(&va.ident); + if basic_item.label.is_empty() { + basic_item.label = ident_to_label(&variant.ident); } - variants.last_mut().unwrap().push(Variant { name: va.ident.clone(), basic_item }) + variants.last_mut().unwrap().push(Variant { + name: variant.ident.clone(), + basic_item, + }) } let display_arm: Vec<_> = variants .iter() - .map(|vg| vg.iter()) - .flatten() - .map(|v| { - let vn = &v.name; - let vl = &v.basic_item.label; - quote! { #name::#vn => write!(f, #vl), } + .flat_map(|variants| variants.iter()) + .map(|variant| { + let variant_name = &variant.name; + let variant_label = &variant.basic_item.label; + quote! { #name::#variant_name => write!(f, #variant_label), } }) .collect(); @@ -117,9 +119,9 @@ fn derive_enum(enum_attrs: &[Attribute], name: Ident, input: syn::DataEnum) -> s })?; let crate_name = match crate_name { proc_macro_crate::FoundCrate::Itself => quote!(crate), - proc_macro_crate::FoundCrate::Name(n) => { - let i = Ident::new(&n, Span::call_site()); - quote! {#i} + proc_macro_crate::FoundCrate::Name(name) => { + let identifier = Ident::new(&name, Span::call_site()); + quote! { #identifier } } }; @@ -128,36 +130,38 @@ fn derive_enum(enum_attrs: &[Attribute], name: Ident, input: syn::DataEnum) -> s let s = s.trim(); quote! { Some(#s) } } - None => { - quote! { None } - } + None => quote! { None }, }; let group: Vec<_> = variants .iter() - .map(|vg| { - let items = vg + .map(|variants| { + let items = variants .iter() - .map(|v| { - let vname = &v.name; - let vname_str = v.name.to_string(); - let label = &v.basic_item.label; - let docstring = match &v.basic_item.description { + .map(|variant| { + let vname = &variant.name; + let vname_str = variant.name.to_string(); + let label = &variant.basic_item.label; + let docstring = match &variant.basic_item.description { Some(s) => { let s = s.trim(); quote! { Some(::alloc::borrow::Cow::Borrowed(#s)) } } None => quote! { None }, }; - let icon = match &v.basic_item.icon { + let icon = match &variant.basic_item.icon { Some(s) => quote! { Some(::alloc::borrow::Cow::Borrowed(#s)) }, None => quote! { None }, }; - quote! { ( #name::#vname, #crate_name::registry::VariantMetadata { - name: ::alloc::borrow::Cow::Borrowed(#vname_str), - label: ::alloc::borrow::Cow::Borrowed(#label), - docstring: #docstring, - icon: #icon, - }), } + quote! { + ( + #name::#vname, #crate_name::registry::VariantMetadata { + name: ::alloc::borrow::Cow::Borrowed(#vname_str), + label: ::alloc::borrow::Cow::Borrowed(#label), + docstring: #docstring, + icon: #icon, + } + ), + } }) .collect::>(); quote! { &[ #(#items)* ], } diff --git a/node-graph/node-macro/src/lib.rs b/node-graph/node-macro/src/lib.rs index 44d8f7e457..15017fd468 100644 --- a/node-graph/node-macro/src/lib.rs +++ b/node-graph/node-macro/src/lib.rs @@ -19,7 +19,9 @@ mod validation; /// `#[widget(F)]` on a type indicates the type of widget to use to display/edit the type, currently `Radio` and `Dropdown` are supported. /// /// `#[label("Foo")]` on a variant overrides the default UI label (which is otherwise the name converted to title case). All labels are collected into a [`core::fmt::Display`] impl. +/// /// `#[icon("tag"))]` sets the icon to use when a variant is shown in a menu or radio button. +/// /// Doc comments on a variant become tooltip text. #[proc_macro_derive(ChoiceType, attributes(widget, menu_separator, label, icon))] pub fn derive_choice_type(input_item: TokenStream) -> TokenStream { From 7ddb6830efcfb178bca02f61ea4b5e7497b045b2 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 1 May 2025 04:02:07 -0700 Subject: [PATCH 18/18] Update stable node IDs in test --- node-graph/graph-craft/src/proto.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index 99991feb4b..2594eb9ad0 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -938,12 +938,12 @@ mod test { assert_eq!( ids, vec![ - NodeId(8409339180888025381), - NodeId(210279231591542793), - NodeId(11043024792989571946), - NodeId(16261870568621497283), - NodeId(6520148642810552409), - NodeId(8779776256867305756) + NodeId(16997244687192517417), + NodeId(12226224850522777131), + NodeId(9162113827627229771), + NodeId(12793582657066318419), + NodeId(16945623684036608820), + NodeId(2640415155091892458) ] ); }