diff --git a/Cargo.lock b/Cargo.lock index 5afed9afa..42c2c4865 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/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index 12f939a8b..76fe7f5e9 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 1a73d938b..140172a62 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; @@ -86,6 +87,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, node_id, @@ -100,7 +105,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); } @@ -160,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), - 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::() => 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::() => 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::() => fill_type_widget(default_info), - Some(x) if x == TypeId::of::() => gradient_type_widget(default_info), - Some(x) if x == TypeId::of::() => boolean_operation_widget(default_info), - Some(x) if x == TypeId::of::() => centroid_type_widget(default_info), - Some(x) if x == TypeId::of::() => luminance_calculation_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::() => 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::() => 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(&[ @@ -766,292 +796,7 @@ 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 { - 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: 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: 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() { - 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(), - ]); - } - LayoutGroup::Row { widgets }.with_tooltip("Style of layered levels of the noise pattern") -} - -// 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() { - 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)) - .disabled(disabled) - .widget_holder(), - ]); - } - LayoutGroup::Row { widgets }.with_tooltip("Distance function used by the cellular noise") -} - -// 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() { - 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(), - ]); - } - LayoutGroup::Row { widgets }.with_tooltip("Return type of the cellular noise") -} - -// 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() { - 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)) - .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(), - ]); - } - LayoutGroup::Row { widgets }.with_tooltip("Type of domain warp") -} - -// 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; @@ -1086,210 +831,6 @@ pub fn blend_mode_widget(parameter_widgets_info: ParameterWidgetsInfo) -> Layout 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_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::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") -} - -pub fn boolean_operation_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::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 } -} - -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; - - 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; @@ -1365,41 +906,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 @@ -1472,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."); @@ -1514,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), @@ -1527,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 }, @@ -1552,31 +1037,11 @@ 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() { - 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()]); - } + let colors = enum_choice::() + .for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, colors_index, true, context)) + .exposable(false) + .property_row(); let colors_choice_index = match &document_node.inputs[colors_index].as_value() { Some(TaggedValue::SelectiveColorChoice(choice)) => choice, @@ -1606,37 +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 }, + colors, // CMYK LayoutGroup::Row { widgets: cyan }, LayoutGroup::Row { widgets: magenta }, LayoutGroup::Row { widgets: yellow }, LayoutGroup::Row { widgets: black }, // Mode - LayoutGroup::Row { widgets: mode }, + mode, ] } @@ -1661,7 +1109,9 @@ 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 = enum_choice::() + .for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, grid_type_index, true, context)) + .property_row(); let mut widgets = vec![grid_type]; @@ -2107,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 |_| { @@ -2119,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 |_| { @@ -2176,8 +1626,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 = 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 = 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, @@ -2213,7 +1667,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 = line_join_widget(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, @@ -2320,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/portfolio/menu_bar/menu_bar_message_handler.rs b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs index 5139b7552..589840d7b 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) - .map(move |(operation, icon)| MenuBarEntry { - label: operation.to_string(), - icon: Some(icon.into()), + let list = ::list(); + list.into_iter() + .map(|i| i.into_iter()) + .flatten() + .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); + 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/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 8080ac9dc..12b8f17b3 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/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index e92980d27..9affd74bc 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -181,14 +181,18 @@ 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) - .tooltip(operation.to_string()) + 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() { + 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); + let group_folder_type = GroupFolderType::BooleanOperation(*operation); DocumentMessage::GroupSelectedLayers { group_folder_type }.into() }) .widget_holder() diff --git a/node-graph/gcore/src/animation.rs b/node-graph/gcore/src/animation.rs index adb63c6c0..97e20dc6a 100644 --- a/node-graph/gcore/src/animation.rs +++ b/node-graph/gcore/src/animation.rs @@ -2,9 +2,10 @@ 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))] pub enum RealTimeMode { + #[label("UTC")] Utc, Year, Hour, @@ -13,18 +14,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 735ccd41a..4c83a67f6 100644 --- a/node-graph/gcore/src/ops.rs +++ b/node-graph/gcore/src/ops.rs @@ -533,22 +533,16 @@ 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)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[widget(Dropdown)] 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 715beca35..0aaa25f6a 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -34,9 +34,11 @@ 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::ChoiceType)] +#[widget(Dropdown)] pub enum LuminanceCalculation { #[default] + #[label("sRGB")] SRGB, Perceptual, AverageChannels, @@ -44,30 +46,6 @@ pub enum LuminanceCalculation { MaximumChannels, } -impl LuminanceCalculation { - pub fn list() -> [LuminanceCalculation; 5] { - [ - LuminanceCalculation::SRGB, - LuminanceCalculation::Perceptual, - LuminanceCalculation::AverageChannels, - LuminanceCalculation::MinimumChannels, - LuminanceCalculation::MaximumChannels, - ] - } -} - -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)] @@ -844,9 +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)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[widget(Radio)] pub enum RedGreenBlue { #[default] Red, @@ -854,19 +834,11 @@ 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"), - } - } -} - +/// 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)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[widget(Radio)] pub enum RedGreenBlueAlpha { #[default] Red, @@ -875,24 +847,17 @@ 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"), - } - } -} - +/// 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)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[widget(Dropdown)] pub enum NoiseType { #[default] Perlin, + #[label("OpenSimplex2")] OpenSimplex2, + #[label("OpenSimplex2S")] OpenSimplex2S, Cellular, ValueCubic, @@ -900,176 +865,71 @@ pub enum NoiseType { 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)] +#[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, - ] - } -} - +/// 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)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] 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, - ] - } -} - +/// 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)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[widget(Dropdown)] pub enum DomainWarpType { #[default] None, + #[label("OpenSimplex2")] OpenSimplex2, + #[label("OpenSimplex2 Reduced")] 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"), - } - } -} - -impl DomainWarpType { - pub fn list() -> &'static [DomainWarpType; 4] { - &[DomainWarpType::None, DomainWarpType::OpenSimplex2, DomainWarpType::OpenSimplex2Reduced, DomainWarpType::BasicGrid] - } -} - // Aims for interoperable compatibility with: // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27mixr%27%20%3D%20Channel%20Mixer // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Lab%20color%20only-,Channel%20Mixer,-Key%20is%20%27mixr @@ -1169,26 +1029,18 @@ 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))] -#[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, @@ -1197,27 +1049,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 af100723e..f77c3602c 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; @@ -53,6 +54,33 @@ pub struct FieldMetadata { pub number_mode_range: Option<(f64, f64)>, } +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)]]; +} + +pub enum ChoiceWidgetHint { + Dropdown, + RadioButtons, +} + +/// 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 4d2d6cb53..8e275c97d 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -3,7 +3,8 @@ use glam::DVec2; use kurbo::Point; /// 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] @@ -12,41 +13,28 @@ 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::ChoiceType)] +#[widget(Radio)] pub enum BooleanOperation { #[default] + #[icon("BooleanUnion")] Union, + #[icon("BooleanSubtractFront")] SubtractFront, + #[icon("BooleanSubtractBack")] SubtractBack, + #[icon("BooleanIntersect")] Intersect, + #[icon("BooleanDifference")] 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"] - } +pub trait AsU32 { + fn as_u32(&self) -> u32; } - -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"), - } +impl AsU32 for u32 { + fn as_u32(&self) -> u32 { + *self } } @@ -88,7 +76,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, @@ -96,7 +85,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 7312fc062..ffb307749 100644 --- a/node-graph/gcore/src/vector/style.rs +++ b/node-graph/gcore/src/vector/style.rs @@ -5,9 +5,10 @@ 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)] +#[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, @@ -471,7 +473,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, @@ -479,18 +482,19 @@ 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", } } } #[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, @@ -498,12 +502,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", } } } @@ -652,10 +656,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/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index db19e7613..b851392e9 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/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index 99991feb4..2594eb9ad 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) ] ); } diff --git a/node-graph/gstd/src/raster.rs b/node-graph/gstd/src/raster.rs index 7ece1c93d..a8f42007a 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 new file mode 100644 index 000000000..86278c662 --- /dev/null +++ b/node-graph/node-macro/src/derive_choice_type.rs @@ -0,0 +1,202 @@ +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; +use syn::parse::Parse; +use syn::{Attribute, DeriveInput, Expr, LitStr, Meta}; + +pub fn derive_choice_type_impl(input_item: TokenStream) -> syn::Result { + let input = syn::parse2::(input_item).unwrap(); + + match input.data { + 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")), + } +} + +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 tokens: Ident = input.parse()?; + if tokens == "Radio" { + Ok(Self::Radio) + } else if tokens == "Dropdown" { + Ok(Self::Dropdown) + } else { + Err(syn::Error::new_spanned(tokens, "Widget must be either Radio or Dropdown")) + } + } +} + +#[derive(Default)] +struct BasicItem { + label: String, + description: Option, + icon: Option, +} +impl BasicItem { + 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 attribute.path().is_ident("icon") { + let token: LitStr = attribute.parse_args()?; + self.icon = Some(token.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()); + } + } + } + } + Ok(()) + } +} + +struct Variant { + name: Ident, + basic_item: BasicItem, +} + +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 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 variant in &input.variants { + let mut basic_item = BasicItem::default(); + + for attribute in &variant.attrs { + if attribute.path().is_ident("menu_separator") { + attribute.meta.require_path_only()?; + variants.push(Vec::new()); + } + basic_item.read_attribute(attribute)?; + } + + if basic_item.label.is_empty() { + basic_item.label = ident_to_label(&variant.ident); + } + + variants.last_mut().unwrap().push(Variant { + name: variant.ident.clone(), + basic_item, + }) + } + let display_arm: Vec<_> = variants + .iter() + .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(); + + 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(name) => { + let identifier = Ident::new(&name, Span::call_site()); + quote! { #identifier } + } + }; + + 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(|variants| { + let items = variants + .iter() + .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 &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, + } + ), + } + }) + .collect::>(); + quote! { &[ #(#items)* ], } + }) + .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::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 )* + } + } + } + }) +} + +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 67f6fc985..15017fd46 100644 --- a/node-graph/node-macro/src/lib.rs +++ b/node-graph/node-macro/src/lib.rs @@ -10,9 +10,24 @@ use syn::{ }; mod codegen; +mod derive_choice_type; mod parsing; mod validation; +/// Generate meta-information for an enum. +/// +/// `#[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 { + 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. /// /// This works by generating two `impl` blocks for the given struct: diff --git a/proc-macros/Cargo.toml b/proc-macros/Cargo.toml index 084226a0a..923c53b34 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