Skip to content

Add the unit, display_decimal_places, and step parameter widget macro attributes #2706

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 15, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ pub(crate) fn property_from_type(
index: usize,
ty: &Type,
number_options: (Option<f64>, Option<f64>, Option<(f64, f64)>),
unit: Option<&str>,
display_decimal_places: Option<u32>,
step: Option<f64>,
context: &mut NodePropertiesContext,
) -> Result<Vec<LayoutGroup>, Vec<LayoutGroup>> {
let Some(network) = context.network_interface.nested_network(context.selection_network_path) else {
Expand All @@ -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);
Expand All @@ -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
_ => {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -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();
}
Expand All @@ -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::<Vec<_>>();
input_types.sort_by_key(|ty| ty.type_name());
let input_type = input_types.first().cloned();
Expand All @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions node-graph/gcore/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ pub struct FieldMetadata {
pub number_min: Option<f64>,
pub number_max: Option<f64>,
pub number_mode_range: Option<(f64, f64)>,
pub number_display_decimal_places: Option<u32>,
pub number_step: Option<f64>,
pub unit: Option<&'static str>,
}

pub trait ChoiceTypeStatic: Sized + Copy + crate::vector::misc::AsU32 + Send + Sync {
Expand Down
38 changes: 38 additions & 0 deletions node-graph/node-macro/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,41 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
_ => 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()
Expand Down Expand Up @@ -375,6 +410,9 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
number_min: #number_min_values,
number_max: #number_max_values,
number_mode_range: #number_mode_range_values,
number_display_decimal_places: #number_display_decimal_places,
number_step: #number_step,
unit: #unit_suffix,
},
)*
],
Expand Down
65 changes: 64 additions & 1 deletion node-graph/node-macro/src/parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::token::{Comma, RArrow};
use syn::{
AttrStyle, Attribute, Error, Expr, ExprTuple, FnArg, GenericParam, Ident, ItemFn, Lit, LitFloat, LitStr, Meta, Pat, PatIdent, PatType, Path, ReturnType, Type, TypeParam, WhereClause, parse_quote,
AttrStyle, Attribute, Error, Expr, ExprTuple, FnArg, GenericParam, Ident, ItemFn, Lit, LitFloat, LitInt, LitStr, Meta, Pat, PatIdent, PatType, Path, ReturnType, Type, TypeParam, WhereClause,
parse_quote,
};

use crate::codegen::generate_node_code;
Expand Down Expand Up @@ -110,7 +111,10 @@ pub(crate) enum ParsedField {
number_hard_min: Option<LitFloat>,
number_hard_max: Option<LitFloat>,
number_mode_range: Option<ExprTuple>,
number_display_decimal_places: Option<LitInt>,
number_step: Option<LitFloat>,
implementations: Punctuated<Type, Comma>,
unit: Option<LitStr>,
},
Node {
pat_ident: PatIdent,
Expand All @@ -119,7 +123,10 @@ pub(crate) enum ParsedField {
widget_override: ParsedWidgetOverride,
input_type: Type,
output_type: Type,
number_display_decimal_places: Option<LitInt>,
number_step: Option<LitFloat>,
implementations: Punctuated<Implementation, Comma>,
unit: Option<LitStr>,
},
}
#[derive(Debug)]
Expand Down Expand Up @@ -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::<LitStr>().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::<LitInt>().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::<u32>() {
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::<LitFloat>()
.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()
Expand Down Expand Up @@ -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")
Expand All @@ -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,
})
}
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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"),
Expand All @@ -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(),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading