Add a family of 14 new string processing nodes (#4010)

* Add new string processing nodes

* Remove the IntegerCount = u32 type alias

* Add the 'Format Number' node

* Add "Up To" parameter to the 'String Pad' node

* Fix 'String Capitalization' behavior

* Add 'Map String' and 'Read String' nodes

* Add separator_escaping to 'String Repeat'

* Add 'Regex Replace'

* Add 'Regex Match' node

* Regex Find and Regex Find All

* Code review

* Add the 'Escape String' node

* Improve implementations

* Move nodes from logic.rs to text/src/lib.rs

* Clean up migrations

* Fix #[{hard, soft}_{min, max}(...)] node macro attribute

* Improve Data panel number and text display

* Add the 'Query JSON' and 'Query JSON All' nodes

* Add the 'Lorem Ipsum' node and Ipsum library

* Add the 14 nodes back in their final state

* Add handling for fixed decimal places in number formatting

* Code review fixes
This commit is contained in:
Keavon Chambers
2026-04-23 22:46:20 -07:00
committed by GitHub
parent 328c4f272b
commit 6c0af72053
3 changed files with 608 additions and 20 deletions

View File

@@ -2035,6 +2035,7 @@ fn static_node_properties() -> NodeProperties {
map.insert("selective_color_properties".to_string(), Box::new(node_properties::selective_color_properties));
map.insert("exposure_properties".to_string(), Box::new(node_properties::exposure_properties));
map.insert("math_properties".to_string(), Box::new(node_properties::math_properties));
map.insert("format_number_properties".to_string(), Box::new(node_properties::format_number_properties));
map.insert("string_capitalization_properties".to_string(), Box::new(node_properties::string_capitalization_properties));
map.insert("rectangle_properties".to_string(), Box::new(node_properties::rectangle_properties));
map.insert("grid_properties".to_string(), Box::new(node_properties::grid_properties));

View File

@@ -134,18 +134,13 @@ pub fn start_widgets(parameter_widgets_info: ParameterWidgetsInfo) -> Vec<Widget
}
widgets.push(TextLabel::new(name).tooltip_description(description).widget_instance());
let mut blank_assist = blank_assist;
if input.is_exposed() {
if blank_assist {
add_blank_assist(&mut widgets);
blank_assist = false;
}
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
widgets.push(jump_to_source_widget(input, network_interface, selection_network_path));
if blank_assist || input.is_exposed() {
add_blank_assist(&mut widgets);
}
if blank_assist {
add_blank_assist(&mut widgets);
if input.is_exposed() {
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
widgets.push(jump_to_source_widget(input, network_interface, selection_network_path));
}
widgets
@@ -1630,6 +1625,91 @@ pub(crate) fn exposure_properties(node_id: NodeId, context: &mut NodePropertiesC
vec![LayoutGroup::row(exposure), LayoutGroup::row(offset), LayoutGroup::row(gamma_correction)]
}
pub(crate) fn format_number_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::text_nodes::format_number::{DecimalPlacesInput, DecimalSeparatorInput, FixedDecimalsInput, StartAt10000Input, ThousandsSeparatorInput, UseThousandsSeparatorInput};
// Read current values before borrowing context mutably for widgets
let (no_decimals, decimal_sep_value, use_thousands, thousands_sep_value) = match get_document_node(node_id, context) {
Ok(document_node) => {
let decimal_places = match document_node.inputs.get(DecimalPlacesInput::INDEX).and_then(|input| input.as_value()) {
Some(&TaggedValue::U32(x)) => x,
_ => 2,
};
let decimal_sep = match document_node.inputs.get(DecimalSeparatorInput::INDEX).and_then(|input| input.as_non_exposed_value()) {
Some(TaggedValue::String(x)) => Some(x.clone()),
_ => None,
};
let use_thousands = match document_node.inputs.get(UseThousandsSeparatorInput::INDEX).and_then(|input| input.as_value()) {
Some(&TaggedValue::Bool(x)) => x,
_ => false,
};
let use_thousands = use_thousands || document_node.inputs.get(ThousandsSeparatorInput::INDEX).is_some_and(|input| input.is_exposed());
let thousands_sep = match document_node.inputs.get(ThousandsSeparatorInput::INDEX).and_then(|input| input.as_non_exposed_value()) {
Some(TaggedValue::String(x)) => Some(x.clone()),
_ => None,
};
(decimal_places == 0, decimal_sep, use_thousands, thousands_sep)
}
Err(err) => {
log::error!("Could not get document node in format_number_properties: {err}");
return Vec::new();
}
};
let decimal_places = number_widget(ParameterWidgetsInfo::new(node_id, DecimalPlacesInput::INDEX, true, context), NumberInput::default().min(0.).int());
// Fixed decimals and decimal separator are disabled when decimal places is 0
let fixed_decimals = bool_widget(
ParameterWidgetsInfo::new(node_id, FixedDecimalsInput::INDEX, true, context),
CheckboxInput::default().disabled(no_decimals),
);
let mut decimal_sep_widgets = start_widgets(ParameterWidgetsInfo::new(node_id, DecimalSeparatorInput::INDEX, true, context));
if let Some(sep) = decimal_sep_value {
decimal_sep_widgets.extend_from_slice(&[
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
TextInput::new(sep)
.disabled(no_decimals)
.on_update(update_value(|x: &TextInput| TaggedValue::String(x.value.clone()), node_id, DecimalSeparatorInput::INDEX))
.on_commit(commit_value)
.widget_instance(),
]);
}
// Thousands separator: checkbox in assist area
let mut thousands_sep_widgets = start_widgets(ParameterWidgetsInfo::new(node_id, ThousandsSeparatorInput::INDEX, false, context));
if let Some(sep) = thousands_sep_value {
thousands_sep_widgets.extend_from_slice(&[
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
CheckboxInput::new(use_thousands)
.on_update(update_value(|x: &CheckboxInput| TaggedValue::Bool(x.checked), node_id, UseThousandsSeparatorInput::INDEX))
.on_commit(commit_value)
.widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
TextInput::new(sep)
.disabled(!use_thousands)
.on_update(update_value(|x: &TextInput| TaggedValue::String(x.value.clone()), node_id, ThousandsSeparatorInput::INDEX))
.on_commit(commit_value)
.widget_instance(),
]);
}
// Start at 10,000: disabled when thousands separator is off
let start_at_10000 = bool_widget(
ParameterWidgetsInfo::new(node_id, StartAt10000Input::INDEX, true, context),
CheckboxInput::default().disabled(!use_thousands),
);
vec![
LayoutGroup::row(decimal_places),
LayoutGroup::row(decimal_sep_widgets),
LayoutGroup::row(fixed_decimals),
LayoutGroup::row(thousands_sep_widgets),
LayoutGroup::row(start_at_10000),
]
}
pub(crate) fn string_capitalization_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::text_nodes::string_capitalization::*;