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 8667ec3394..f2c4700dbc 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -121,6 +121,9 @@ pub(crate) fn property_from_type( index: usize, ty: &Type, number_options: (Option, Option, Option<(f64, f64)>), + unit: Option<&str>, + display_decimal_places: Option, + step: Option, context: &mut NodePropertiesContext, ) -> Result, Vec> { let Some(network) = context.network_interface.nested_network(context.selection_network_path) else { @@ -142,6 +145,15 @@ pub(crate) fn property_from_type( number_max = Some(range_end); number_input = number_input.mode_range().min(range_start).max(range_end); } + if let Some(unit) = unit { + number_input = number_input.unit(unit); + } + if let Some(display_decimal_places) = display_decimal_places { + number_input = number_input.display_decimal_places(display_decimal_places); + } + if let Some(step) = step { + number_input = number_input.step(step); + } let min = |x: f64| number_min.unwrap_or(x); let max = |x: f64| number_max.unwrap_or(x); @@ -155,15 +167,15 @@ pub(crate) fn property_from_type( // Aliased types (ambiguous values) Some("Percentage") => number_widget(default_info, number_input.percentage().min(min(0.)).max(max(100.))).into(), Some("SignedPercentage") => number_widget(default_info, number_input.percentage().min(min(-100.)).max(max(100.))).into(), - Some("Angle") => number_widget(default_info, number_input.mode_range().min(min(-180.)).max(max(180.)).unit("°")).into(), - Some("Multiplier") => number_widget(default_info, number_input.unit("x")).into(), - Some("PixelLength") => number_widget(default_info, number_input.min(min(0.)).unit(" px")).into(), + Some("Angle") => number_widget(default_info, number_input.mode_range().min(min(-180.)).max(max(180.)).unit(unit.unwrap_or("°"))).into(), + Some("Multiplier") => number_widget(default_info, number_input.unit(unit.unwrap_or("x"))).into(), + Some("PixelLength") => number_widget(default_info, number_input.min(min(0.)).unit(unit.unwrap_or(" px"))).into(), Some("Length") => number_widget(default_info, number_input.min(min(0.))).into(), Some("Fraction") => number_widget(default_info, number_input.mode_range().min(min(0.)).max(max(1.))).into(), Some("IntegerCount") => number_widget(default_info, number_input.int().min(min(1.))).into(), Some("SeedValue") => number_widget(default_info, number_input.int().min(min(0.))).into(), - Some("Resolution") => coordinate_widget(default_info, "W", "H", " px", Some(64.)), - Some("PixelSize") => coordinate_widget(default_info, "X", "Y", " px", None), + Some("Resolution") => coordinate_widget(default_info, "W", "H", unit.unwrap_or(" px"), Some(64.)), + Some("PixelSize") => coordinate_widget(default_info, "X", "Y", unit.unwrap_or(" px"), None), // For all other types, use TypeId-based matching _ => { @@ -249,8 +261,8 @@ pub(crate) fn property_from_type( } } Type::Generic(_) => vec![TextLabel::new("Generic type (not supported)").widget_holder()].into(), - Type::Fn(_, out) => return property_from_type(node_id, index, out, number_options, context), - Type::Future(out) => return property_from_type(node_id, index, out, number_options, context), + Type::Fn(_, out) => return property_from_type(node_id, index, out, number_options, unit, display_decimal_places, step, context), + Type::Future(out) => return property_from_type(node_id, index, out, number_options, unit, display_decimal_places, step, context), }; extra_widgets.push(widgets); @@ -1395,6 +1407,9 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper }; let mut number_options = (None, None, None); + let mut display_decimal_places = None; + let mut step = None; + let mut unit_suffix = None; let input_type = match implementation { DocumentNodeImplementation::ProtoNode(proto_node_identifier) => 'early_return: { if let Some(field) = graphene_core::registry::NODE_METADATA @@ -1404,6 +1419,9 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper .and_then(|metadata| metadata.fields.get(input_index)) { number_options = (field.number_min, field.number_max, field.number_mode_range); + display_decimal_places = field.number_display_decimal_places; + unit_suffix = field.unit; + step = field.number_step; if let Some(ref default) = field.default_type { break 'early_return default.clone(); } @@ -1417,7 +1435,7 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper let mut input_types = implementations .keys() .filter_map(|item| item.inputs.get(input_index)) - .filter(|ty| property_from_type(node_id, input_index, ty, number_options, context).is_ok()) + .filter(|ty| property_from_type(node_id, input_index, ty, number_options, unit_suffix, display_decimal_places, step, context).is_ok()) .collect::>(); input_types.sort_by_key(|ty| ty.type_name()); let input_type = input_types.first().cloned(); @@ -1431,7 +1449,7 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper _ => context.network_interface.input_type(&InputConnector::node(node_id, input_index), context.selection_network_path).0, }; - property_from_type(node_id, input_index, &input_type, number_options, context).unwrap_or_else(|value| value) + property_from_type(node_id, input_index, &input_type, number_options, unit_suffix, display_decimal_places, step, context).unwrap_or_else(|value| value) }); layout.extend(row); diff --git a/node-graph/gcore/src/registry.rs b/node-graph/gcore/src/registry.rs index a968cfc842..9a773cb51a 100644 --- a/node-graph/gcore/src/registry.rs +++ b/node-graph/gcore/src/registry.rs @@ -54,6 +54,9 @@ pub struct FieldMetadata { pub number_min: Option, pub number_max: Option, pub number_mode_range: Option<(f64, f64)>, + pub number_display_decimal_places: Option, + pub number_step: Option, + pub unit: Option<&'static str>, } pub trait ChoiceTypeStatic: Sized + Copy + crate::vector::misc::AsU32 + Send + Sync { diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 74d45ff0a2..b6a881fe9d 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -163,6 +163,41 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result quote!(None), }) .collect(); + let number_display_decimal_places: Vec<_> = fields + .iter() + .map(|field| match field { + ParsedField::Regular { + number_display_decimal_places: Some(decimal_places), + .. + } + | ParsedField::Node { + number_display_decimal_places: Some(decimal_places), + .. + } => { + quote!(Some(#decimal_places)) + } + _ => quote!(None), + }) + .collect(); + let number_step: Vec<_> = fields + .iter() + .map(|field| match field { + ParsedField::Regular { number_step: Some(step), .. } | ParsedField::Node { number_step: Some(step), .. } => { + quote!(Some(#step)) + } + _ => quote!(None), + }) + .collect(); + + let unit_suffix: Vec<_> = fields + .iter() + .map(|field| match field { + ParsedField::Regular { unit: Some(unit), .. } | ParsedField::Node { unit: Some(unit), .. } => { + quote!(Some(#unit)) + } + _ => quote!(None), + }) + .collect(); let exposed: Vec<_> = fields .iter() @@ -375,6 +410,9 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result, number_hard_max: Option, number_mode_range: Option, + number_display_decimal_places: Option, + number_step: Option, implementations: Punctuated, + unit: Option, }, Node { pat_ident: PatIdent, @@ -119,7 +123,10 @@ pub(crate) enum ParsedField { widget_override: ParsedWidgetOverride, input_type: Type, output_type: Type, + number_display_decimal_places: Option, + number_step: Option, implementations: Punctuated, + unit: Option, }, } #[derive(Debug)] @@ -466,6 +473,35 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul } } + let unit = extract_attribute(attrs, "unit") + .map(|attr| attr.parse_args::().map_err(|e| Error::new_spanned(attr, format!("Expected a unit type as string")))) + .transpose()?; + + let number_display_decimal_places = extract_attribute(attrs, "display_decimal_places") + .map(|attr| { + attr.parse_args::().map_err(|e| { + Error::new_spanned( + attr, + format!("Invalid `integer` for number of decimals for argument '{}': {}\nUSAGE EXAMPLE: #[display_decimal_places(2)]", ident, e), + ) + }) + }) + .transpose()? + .map(|f| { + if let Err(e) = f.base10_parse::() { + Err(Error::new_spanned(f, format!("Expected a `u32` for `display_decimal_places` for '{}': {}", ident, e))) + } else { + Ok(f) + } + }) + .transpose()?; + let number_step = extract_attribute(attrs, "step") + .map(|attr| { + attr.parse_args::() + .map_err(|e| Error::new_spanned(attr, format!("Invalid `step` for argument '{}': {}\nUSAGE EXAMPLE: #[step(2.)]", ident, e))) + }) + .transpose()?; + let (is_node, node_input_type, node_output_type) = parse_node_type(&ty); let description = attrs .iter() @@ -502,7 +538,10 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul widget_override, input_type, output_type, + number_display_decimal_places, + number_step, implementations, + unit, }) } else { let implementations = extract_attribute(attrs, "implementations") @@ -520,9 +559,12 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul number_hard_min, number_hard_max, number_mode_range, + number_display_decimal_places, + number_step, ty, value_source, implementations, + unit, }) } } @@ -738,7 +780,10 @@ mod tests { number_hard_min: None, number_hard_max: None, number_mode_range: None, + number_display_decimal_places: None, + number_step: None, implementations: Punctuated::new(), + unit: None, }], body: TokenStream2::new(), crate_name: FoundCrate::Itself, @@ -790,7 +835,10 @@ mod tests { widget_override: ParsedWidgetOverride::None, input_type: parse_quote!(Footprint), output_type: parse_quote!(T), + number_display_decimal_places: None, + number_step: None, implementations: Punctuated::new(), + unit: None, }, ParsedField::Regular { pat_ident: pat_ident("translate"), @@ -805,7 +853,10 @@ mod tests { number_hard_min: None, number_hard_max: None, number_mode_range: None, + number_display_decimal_places: None, + number_step: None, implementations: Punctuated::new(), + unit: None, }, ], body: TokenStream2::new(), @@ -860,7 +911,10 @@ mod tests { number_hard_min: None, number_hard_max: None, number_mode_range: None, + number_display_decimal_places: None, + number_step: None, implementations: Punctuated::new(), + unit: None, }], body: TokenStream2::new(), crate_name: FoundCrate::Itself, @@ -913,12 +967,15 @@ mod tests { number_hard_min: None, number_hard_max: None, number_mode_range: None, + number_display_decimal_places: None, + number_step: None, implementations: { let mut p = Punctuated::new(); p.push(parse_quote!(f32)); p.push(parse_quote!(f64)); p }, + unit: None, }], body: TokenStream2::new(), crate_name: FoundCrate::Itself, @@ -978,7 +1035,10 @@ mod tests { number_hard_min: None, number_hard_max: None, number_mode_range: Some(parse_quote!((0., 100.))), + number_display_decimal_places: None, + number_step: None, implementations: Punctuated::new(), + unit: None, }], body: TokenStream2::new(), crate_name: FoundCrate::Itself, @@ -1031,7 +1091,10 @@ mod tests { number_hard_min: None, number_hard_max: None, number_mode_range: None, + number_display_decimal_places: None, + number_step: None, implementations: Punctuated::new(), + unit: None, }], body: TokenStream2::new(), crate_name: FoundCrate::Itself,