From f33aaa9369fd20f6bb3d88a14934cc2eaa703116 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 15 Jan 2026 14:37:27 -0800 Subject: [PATCH 1/7] WIP --- .../document/graph_operation/utility_types.rs | 6 +- .../node_graph/document_node_definitions.rs | 122 ++++-------------- .../document/node_graph/node_properties.rs | 29 ----- .../messages/portfolio/document_migration.rs | 28 +++- .../graph_modification_utils.rs | 16 ++- .../messages/tool/tool_messages/text_tool.rs | 13 +- node-graph/graph-craft/src/document/value.rs | 4 +- .../libraries/graphic-types/src/graphic.rs | 12 -- node-graph/nodes/blending/src/lib.rs | 1 - node-graph/nodes/gstd/src/text.rs | 65 +++++++--- .../nodes/gstd/src/wasm_application_io.rs | 2 +- node-graph/nodes/math/src/lib.rs | 1 - 12 files changed, 121 insertions(+), 178 deletions(-) diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index b0c8760073..d364a78774 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -206,8 +206,10 @@ impl<'a> ModifyInputsContext<'a> { Some(NodeInput::value(TaggedValue::F64(typesetting.font_size), false)), Some(NodeInput::value(TaggedValue::F64(typesetting.line_height_ratio), false)), Some(NodeInput::value(TaggedValue::F64(typesetting.character_spacing), false)), - Some(NodeInput::value(TaggedValue::OptionalF64(typesetting.max_width), false)), - Some(NodeInput::value(TaggedValue::OptionalF64(typesetting.max_height), false)), + Some(NodeInput::value(TaggedValue::Bool(typesetting.max_width.is_some()), false)), + Some(NodeInput::value(TaggedValue::F64(typesetting.max_width.unwrap_or(100.)), false)), + Some(NodeInput::value(TaggedValue::Bool(typesetting.max_width.is_some()), false)), + Some(NodeInput::value(TaggedValue::F64(typesetting.max_width.unwrap_or(100.)), false)), Some(NodeInput::value(TaggedValue::F64(typesetting.tilt), false)), Some(NodeInput::value(TaggedValue::TextAlign(typesetting.align), false)), ]); 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 3f0171e83b..cd36bf7c4d 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 @@ -21,7 +21,6 @@ use graphene_std::extract_xy::XY; use graphene_std::raster::{CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, NoiseType, RedGreenBlueAlpha}; use graphene_std::raster_types::{CPU, Raster}; use graphene_std::table::Table; -use graphene_std::text::{Font, TypesettingConfig}; #[allow(unused_imports)] use graphene_std::transform::Footprint; use graphene_std::vector::Vector; @@ -1653,103 +1652,6 @@ fn document_node_definitions() -> HashMap InputProperties { }]) }), ); + map.insert( + "optional_number".to_string(), + Box::new(|node_id, index, context| { + // TODO: Don't wipe out the previously set value (setting it back to the default of 100) when reenabling this checkbox back to Some from None + let toggle_enabled = move |checkbox_input: &CheckboxInput| TaggedValue::OptionalF64(if checkbox_input.checked { Some(100.) } else { None }); + Ok(vec![ + Separator::new(SeparatorStyle::Unrelated).widget_instance(), + Separator::new(SeparatorStyle::Related).widget_instance(), + // The checkbox toggles if the value is Some or None + CheckboxInput::new(x.is_some()) + .on_update(update_value(toggle_enabled, node_id, index)) + .on_commit(commit_value) + .widget_instance(), + Separator::new(SeparatorStyle::Related).widget_instance(), + Separator::new(SeparatorStyle::Unrelated).widget_instance(), + number_props + .value(x) + .on_update(update_value(move |x: &NumberInput| TaggedValue::OptionalF64(x.value), node_id, index)) + .disabled(x.is_none()) + .on_commit(commit_value) + .widget_instance(), + ]) + }), + ); map.insert( "vec2".to_string(), Box::new(|node_id, index, context| { 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 661215ee11..9e3cb659d3 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -982,27 +982,6 @@ pub fn number_widget(parameter_widgets_info: ParameterWidgetsInfo, number_props: .on_commit(commit_value) .widget_instance(), ]), - Some(&TaggedValue::OptionalF64(x)) => { - // TODO: Don't wipe out the previously set value (setting it back to the default of 100) when reenabling this checkbox back to Some from None - let toggle_enabled = move |checkbox_input: &CheckboxInput| TaggedValue::OptionalF64(if checkbox_input.checked { Some(100.) } else { None }); - widgets.extend_from_slice(&[ - Separator::new(SeparatorStyle::Unrelated).widget_instance(), - Separator::new(SeparatorStyle::Related).widget_instance(), - // The checkbox toggles if the value is Some or None - CheckboxInput::new(x.is_some()) - .on_update(update_value(toggle_enabled, node_id, index)) - .on_commit(commit_value) - .widget_instance(), - Separator::new(SeparatorStyle::Related).widget_instance(), - Separator::new(SeparatorStyle::Unrelated).widget_instance(), - number_props - .value(x) - .on_update(update_value(move |x: &NumberInput| TaggedValue::OptionalF64(x.value), node_id, index)) - .disabled(x.is_none()) - .on_commit(commit_value) - .widget_instance(), - ]); - } Some(&TaggedValue::DVec2(dvec2)) => widgets.extend_from_slice(&[ Separator::new(SeparatorStyle::Unrelated).widget_instance(), number_props @@ -1087,14 +1066,6 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button: .on_commit(commit_value) .widget_instance(), ), - TaggedValue::OptionalColorNotInTable(color) => widgets.push( - color_button - .value(color.map_or(FillChoice::None, FillChoice::Solid)) - .allow_none(true) - .on_update(update_value(|input: &ColorInput| TaggedValue::OptionalColorNotInTable(input.value.as_solid()), node_id, index)) - .on_commit(commit_value) - .widget_instance(), - ), TaggedValue::Color(color_table) => widgets.push( color_button .value(match color_table.iter().next() { diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index c19c187dc3..d3f511e2f6 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -1166,7 +1166,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], if inputs_count >= 7 { old_inputs[6].clone() } else { - NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_width), false) + NodeInput::value(TaggedValue::Bool(TypesettingConfig::default().max_width.is_some()), false) }, network_path, ); @@ -1175,7 +1175,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], if inputs_count >= 8 { old_inputs[7].clone() } else { - NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_height), false) + NodeInput::value(TaggedValue::F64(TypesettingConfig::default().max_width.unwrap_or(100.)), false) }, network_path, ); @@ -1184,16 +1184,16 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], if inputs_count >= 9 { old_inputs[8].clone() } else { - NodeInput::value(TaggedValue::F64(TypesettingConfig::default().tilt), false) + NodeInput::value(TaggedValue::Bool(TypesettingConfig::default().max_height.is_some()), false) }, network_path, ); document.network_interface.set_input( &InputConnector::node(*node_id, 9), - if inputs_count >= 11 { + if inputs_count >= 10 { old_inputs[9].clone() } else { - NodeInput::value(TaggedValue::TextAlign(TextAlign::default()), false) + NodeInput::value(TaggedValue::F64(TypesettingConfig::default().max_height.unwrap_or(100.)), false) }, network_path, ); @@ -1201,6 +1201,24 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], &InputConnector::node(*node_id, 10), if inputs_count >= 11 { old_inputs[10].clone() + } else { + NodeInput::value(TaggedValue::F64(TypesettingConfig::default().tilt), false) + }, + network_path, + ); + document.network_interface.set_input( + &InputConnector::node(*node_id, 11), + if inputs_count >= 12 { + old_inputs[11].clone() + } else { + NodeInput::value(TaggedValue::TextAlign(TextAlign::default()), false) + }, + network_path, + ); + document.network_interface.set_input( + &InputConnector::node(*node_id, 12), + if inputs_count >= 13 { + old_inputs[12].clone() } else { NodeInput::value(TaggedValue::Bool(false), false) }, diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index 98386bb0ad..a8c2fb6e2d 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -393,18 +393,20 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter let Some(&TaggedValue::F64(font_size)) = inputs[3].as_value() else { return None }; let Some(&TaggedValue::F64(line_height_ratio)) = inputs[4].as_value() else { return None }; let Some(&TaggedValue::F64(character_spacing)) = inputs[5].as_value() else { return None }; - let Some(&TaggedValue::OptionalF64(max_width)) = inputs[6].as_value() else { return None }; - let Some(&TaggedValue::OptionalF64(max_height)) = inputs[7].as_value() else { return None }; - let Some(&TaggedValue::F64(tilt)) = inputs[8].as_value() else { return None }; - let Some(&TaggedValue::TextAlign(align)) = inputs[9].as_value() else { return None }; - let Some(&TaggedValue::Bool(per_glyph_instances)) = inputs[10].as_value() else { return None }; + let Some(&TaggedValue::Bool(has_max_width)) = inputs[6].as_value() else { return None }; + let Some(&TaggedValue::F64(max_width)) = inputs[7].as_value() else { return None }; + let Some(&TaggedValue::Bool(has_max_height)) = inputs[8].as_value() else { return None }; + let Some(&TaggedValue::F64(max_height)) = inputs[9].as_value() else { return None }; + let Some(&TaggedValue::F64(tilt)) = inputs[10].as_value() else { return None }; + let Some(&TaggedValue::TextAlign(align)) = inputs[11].as_value() else { return None }; + let Some(&TaggedValue::Bool(per_glyph_instances)) = inputs[12].as_value() else { return None }; let typesetting = TypesettingConfig { font_size, line_height_ratio, - max_width, + max_width: has_max_width.then_some(max_width), + max_height: has_max_height.then_some(max_height), character_spacing, - max_height, tilt, align, }; diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index 49a7bdfd5f..75033194de 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -800,13 +800,22 @@ impl Fsm for TextToolFsmState { // Find the translation necessary from the original position in viewport space let translation_viewport = bounds.original_bound_transform.transform_vector2(translation_bounds_space); + // TODO: Don't set both max_width and max_height to true at the same time, only do one based on which edge is being dragged (or both if a corner is being dragged) responses.add(NodeGraphMessage::SetInput { input_connector: InputConnector::node(node_id, 6), - input: NodeInput::value(TaggedValue::OptionalF64(Some(size_layer.x)), false), + input: NodeInput::value(TaggedValue::Bool(true), false), }); responses.add(NodeGraphMessage::SetInput { input_connector: InputConnector::node(node_id, 7), - input: NodeInput::value(TaggedValue::OptionalF64(Some(size_layer.y)), false), + input: NodeInput::value(TaggedValue::F64(size_layer.x), false), + }); + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 8), + input: NodeInput::value(TaggedValue::Bool(true), false), + }); + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 9), + input: NodeInput::value(TaggedValue::F64(size_layer.y), false), }); responses.add(GraphOperationMessage::TransformSet { layer: dragging_layer.id, diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 16827f1926..72e81f874d 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -173,9 +173,7 @@ tagged_value! { U64(u64), Bool(bool), String(String), - OptionalF64(Option), ColorNotInTable(Color), - OptionalColorNotInTable(Option), // ======================== // LISTS OF PRIMITIVE TYPES // ======================== @@ -366,9 +364,9 @@ impl TaggedValue { x if x == TypeId::of::() => FromStr::from_str(string).map(TaggedValue::U32).ok()?, x if x == TypeId::of::() => to_dvec2(string).map(TaggedValue::DVec2)?, x if x == TypeId::of::() => FromStr::from_str(string).map(TaggedValue::Bool).ok()?, - x if x == TypeId::of::>() => to_color(string).map(|color| TaggedValue::Color(Table::new_from_element(color)))?, x if x == TypeId::of::() => to_color(string).map(TaggedValue::ColorNotInTable)?, x if x == TypeId::of::>() => TaggedValue::ColorNotInTable(to_color(string)?), + x if x == TypeId::of::>() => to_color(string).map(|color| TaggedValue::Color(Table::new_from_element(color)))?, x if x == TypeId::of::() => to_color(string).map(|color| TaggedValue::Fill(Fill::solid(color)))?, x if x == TypeId::of::() => to_reference_point(string).map(TaggedValue::ReferencePoint)?, _ => return None, diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index d5db099745..c535ed461a 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -90,18 +90,6 @@ impl From> for Graphic { } } // Note: Table conversions handled by blanket impl in gcore - -// Option -impl From> for Graphic { - fn from(color: Option) -> Self { - if let Some(color) = color { - Graphic::Color(Table::new_from_element(color)) - } else { - Graphic::default() - } - } -} -// Note: Table conversions handled by blanket impl in gcore // Note: Table -> Option is in gcore (Color is defined there) // GradientStops diff --git a/node-graph/nodes/blending/src/lib.rs b/node-graph/nodes/blending/src/lib.rs index 6c07eb5490..a770c85b5e 100644 --- a/node-graph/nodes/blending/src/lib.rs +++ b/node-graph/nodes/blending/src/lib.rs @@ -244,7 +244,6 @@ fn blending( #[default(100.)] fill: Percentage, /// Whether the content inherits the alpha of the content beneath it. - #[default(false)] clip: bool, ) -> T { // TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or TableRow) rather than applying to each row in its own table, which produces the undesired result diff --git a/node-graph/nodes/gstd/src/text.rs b/node-graph/nodes/gstd/src/text.rs index 9dfe656351..5a085e7295 100644 --- a/node-graph/nodes/gstd/src/text.rs +++ b/node-graph/nodes/gstd/src/text.rs @@ -3,41 +3,72 @@ use graph_craft::wasm_application_io::WasmEditorApi; use graphic_types::Vector; pub use text_nodes::*; -#[node_macro::node(category(""))] +/// Draws a text string as vector geometry with a choice of font and styling. +#[node_macro::node(category("Text"))] fn text<'i: 'n>( _: impl Ctx, - editor: &'i WasmEditorApi, + /// The Graphite editor's source for global font resources. + #[scope("editor-api")] + editor_resources: &'i WasmEditorApi, + /// The text content to be drawn. + #[widget(ParsedWidgetOverride::Custom = "text_area")] + #[default("Lorem ipsum")] text: String, + /// The typeface used to draw the text. + #[widget(ParsedWidgetOverride::Custom = "text_font")] font: Font, + /// The font size used to draw the text. #[unit(" px")] #[default(24.)] - font_size: f64, + #[hard_min(1.)] + size: f64, + /// The line height ratio, relative to the font size. Each line is drawn lower than its previous line by the distance of *Size* × *Line Height*. + /// + /// 0 means all lines overlap. 1 means all lines are spaced by just the font size. 1.2 is a common default for readable text. 2 means double-spaced text. #[unit("x")] + #[hard_min(0.)] + #[step(0.1)] #[default(1.2)] - line_height_ratio: f64, + line_height: f64, + /// Additional spacing, in pixels, added between each character. #[unit(" px")] - #[default(0.)] + #[step(0.1)] character_spacing: f64, - #[unit(" px")] max_width: Option, - #[unit(" px")] max_height: Option, - /// Faux italic. + /// Whether the *Max Width* property is enabled so that lines can wrap to fit its specified block width. + #[widget(ParsedWidgetOverride::Hidden)] + has_max_width: bool, + /// The maximum width that the text block can occupy before wrapping to a new line. Otherwise, lines do not wrap. + #[unit(" px")] + #[widget(ParsedWidgetOverride::Custom = "optional_number")] + max_width: f64, + /// Whether the *Max Height* property is enabled so that lines beyond it are not drawn. + #[widget(ParsedWidgetOverride::Hidden)] + has_max_height: bool, + /// The maximum height that the text block can occupy. Excess lines are not drawn. + #[unit(" px")] + #[widget(ParsedWidgetOverride::Custom = "optional_number")] + max_height: f64, + /// The angle of faux italic slant applied to each glyph. #[unit("°")] - #[default(0.)] + #[hard_min(-85.)] + #[hard_max(85.)] tilt: f64, + /// The horizontal alignment of each line of text within its surrounding box. + /// To have an effect on a single line of text, *Max Width* must be set. + #[widget(ParsedWidgetOverride::Custom = "text_align")] align: TextAlign, - /// Splits each text glyph into its own row in the table of vector geometry. - #[default(false)] - per_glyph_instances: bool, + /// Whether to split every letterform into its own vector path element. Otherwise, a single compound path is produced. + separate_glyph_elements: bool, ) -> Table { let typesetting = TypesettingConfig { - font_size, - line_height_ratio, + font_size: size, + line_height_ratio: line_height, character_spacing, - max_width, - max_height, + max_width: Some(max_width), + max_height: Some(max_height), tilt, align, }; - to_path(&text, &font, &editor.font_cache, typesetting, per_glyph_instances) + to_path(&text, &font, &editor_resources.font_cache, typesetting, separate_glyph_elements) } diff --git a/node-graph/nodes/gstd/src/wasm_application_io.rs b/node-graph/nodes/gstd/src/wasm_application_io.rs index 3fd0dd13dd..df5d2b1955 100644 --- a/node-graph/nodes/gstd/src/wasm_application_io.rs +++ b/node-graph/nodes/gstd/src/wasm_application_io.rs @@ -131,7 +131,7 @@ fn image_to_bytes(_: impl Ctx, image: Table>) -> Vec { image.element.data.iter().flat_map(|color| color.to_rgb8_srgb().into_iter()).collect::>() } -/// Loads binary from URLs and local asset paths. Returns a transparent placeholder if the resource fails to load, allowing workflows to continue. +/// Loads binary from URLs and local asset paths. Returns a transparent placeholder if the resource fails to load, allowing rendering to continue. #[node_macro::node(category("Web Request"))] async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[scope("editor-api")] editor_resources: &'a WasmEditorApi, #[name("URL")] url: String) -> Arc<[u8]> { let Some(api) = editor_resources.application_io.as_ref() else { diff --git a/node-graph/nodes/math/src/lib.rs b/node-graph/nodes/math/src/lib.rs index 93c4fc2950..ef8bee97d3 100644 --- a/node-graph/nodes/math/src/lib.rs +++ b/node-graph/nodes/math/src/lib.rs @@ -405,7 +405,6 @@ fn random( /// Seed to determine the unique variation of which number is generated. seed: u64, /// The smaller end of the range within which the random number is generated. - #[default(0.)] min: f64, /// The larger end of the range within which the random number is generated. #[default(1.)] From cf6abc7668821022ef0786504b49c9576cf95043 Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 15 Jan 2026 15:26:07 -0800 Subject: [PATCH 2/7] Fix widget --- .../node_graph/document_node_definitions.rs | 25 +++-------- .../document/node_graph/node_properties.rs | 44 +++++++++++++++++++ node-graph/nodes/gstd/src/text.rs | 8 ++-- 3 files changed, 53 insertions(+), 24 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index cd36bf7c4d..d23325f7d8 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 @@ -2200,27 +2200,12 @@ fn static_input_properties() -> InputProperties { }), ); map.insert( - "optional_number".to_string(), + "optional_f64".to_string(), Box::new(|node_id, index, context| { - // TODO: Don't wipe out the previously set value (setting it back to the default of 100) when reenabling this checkbox back to Some from None - let toggle_enabled = move |checkbox_input: &CheckboxInput| TaggedValue::OptionalF64(if checkbox_input.checked { Some(100.) } else { None }); - Ok(vec![ - Separator::new(SeparatorStyle::Unrelated).widget_instance(), - Separator::new(SeparatorStyle::Related).widget_instance(), - // The checkbox toggles if the value is Some or None - CheckboxInput::new(x.is_some()) - .on_update(update_value(toggle_enabled, node_id, index)) - .on_commit(commit_value) - .widget_instance(), - Separator::new(SeparatorStyle::Related).widget_instance(), - Separator::new(SeparatorStyle::Unrelated).widget_instance(), - number_props - .value(x) - .on_update(update_value(move |x: &NumberInput| TaggedValue::OptionalF64(x.value), node_id, index)) - .disabled(x.is_none()) - .on_commit(commit_value) - .widget_instance(), - ]) + Ok(vec![LayoutGroup::Row { + // The bool input must be directly above the number + widgets: node_properties::optional_f64_widget(ParameterWidgetsInfo::new(node_id, index, false, context), index - 1, NumberInput::default()), + }]) }), ); map.insert( 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 9e3cb659d3..ae1f65c569 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -939,6 +939,50 @@ pub fn progression_widget(parameter_widgets_info: ParameterWidgetsInfo, number_p widgets } +/// ParameterWidgetsInfo is for the number +/// Bool index is the input index of the bool parameter displayed in the checkbox +pub fn optional_f64_widget(parameter_widgets_info: ParameterWidgetsInfo, bool_input_index: usize, number_props: NumberInput) -> Vec { + let ParameterWidgetsInfo { + document_node, + node_id, + index: number_input_index, + .. + } = parameter_widgets_info; + + let mut widgets = start_widgets(parameter_widgets_info); + + let Some(document_node) = document_node else { return Vec::new() }; + let Some(number_input) = document_node.inputs.get(number_input_index) else { + log::warn!("A widget failed to be built because its node's input index is invalid."); + return vec![]; + }; + let Some(bool_input) = document_node.inputs.get(bool_input_index) else { + log::warn!("A widget failed to be built because its node's input index is invalid."); + return vec![]; + }; + if let (Some(&TaggedValue::Bool(enabled)), Some(&TaggedValue::F64(number))) = (bool_input.as_non_exposed_value(), number_input.as_non_exposed_value()) { + widgets.extend_from_slice(&[ + Separator::new(SeparatorStyle::Unrelated).widget_instance(), + Separator::new(SeparatorStyle::Related).widget_instance(), + // The checkbox toggles if the value is Some or None + CheckboxInput::new(enabled) + .on_update(update_value(|x: &CheckboxInput| TaggedValue::Bool(x.checked), node_id, bool_input_index)) + .on_commit(commit_value) + .widget_instance(), + Separator::new(SeparatorStyle::Related).widget_instance(), + Separator::new(SeparatorStyle::Unrelated).widget_instance(), + number_props + .value(Some(number)) + .on_update(update_value(move |x: &NumberInput| TaggedValue::F64(x.value.unwrap_or_default()), node_id, number_input_index)) + .disabled(!enabled) + .on_commit(commit_value) + .widget_instance(), + ]); + } + + widgets +} + pub fn number_widget(parameter_widgets_info: ParameterWidgetsInfo, number_props: NumberInput) -> Vec { let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; diff --git a/node-graph/nodes/gstd/src/text.rs b/node-graph/nodes/gstd/src/text.rs index 5a085e7295..84cba91a9b 100644 --- a/node-graph/nodes/gstd/src/text.rs +++ b/node-graph/nodes/gstd/src/text.rs @@ -39,14 +39,14 @@ fn text<'i: 'n>( has_max_width: bool, /// The maximum width that the text block can occupy before wrapping to a new line. Otherwise, lines do not wrap. #[unit(" px")] - #[widget(ParsedWidgetOverride::Custom = "optional_number")] + #[widget(ParsedWidgetOverride::Custom = "optional_f64")] max_width: f64, /// Whether the *Max Height* property is enabled so that lines beyond it are not drawn. #[widget(ParsedWidgetOverride::Hidden)] has_max_height: bool, /// The maximum height that the text block can occupy. Excess lines are not drawn. #[unit(" px")] - #[widget(ParsedWidgetOverride::Custom = "optional_number")] + #[widget(ParsedWidgetOverride::Custom = "optional_f64")] max_height: f64, /// The angle of faux italic slant applied to each glyph. #[unit("°")] @@ -64,8 +64,8 @@ fn text<'i: 'n>( font_size: size, line_height_ratio: line_height, character_spacing, - max_width: Some(max_width), - max_height: Some(max_height), + max_width: has_max_width.then_some(max_width), + max_height: has_max_height.then_some(max_height), tilt, align, }; From 4ae3114981978a35bda5a086dbd705183044cee6 Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 15 Jan 2026 16:13:31 -0800 Subject: [PATCH 3/7] Fix migration --- .../messages/portfolio/document_migration.rs | 59 +++++++++++++------ node-graph/graph-craft/src/document/value.rs | 1 + 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index d3f511e2f6..275481448a 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -1035,7 +1035,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], // Only nodes that have not been modified and still refer to a definition can be updated let reference = document.network_interface.reference(node_id, network_path)?; - let inputs_count = node.inputs.len(); + let mut inputs_count = node.inputs.len(); // Upgrade Stroke node to reorder parameters and add "Align" and "Paint Order" (#2644) if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER) && inputs_count == 8 { @@ -1134,7 +1134,8 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], } // Upgrade Text node to include line height and character spacing, which were previously hardcoded to 1, from https://github.com/GraphiteEditor/Graphite/pull/2016 - if reference == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER) && inputs_count != 11 { + if reference == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER) && inputs_count == 8 { + log::debug!("first upgrade"); let mut template: NodeTemplate = resolve_document_node_type(&reference)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut template)?; @@ -1184,7 +1185,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], if inputs_count >= 9 { old_inputs[8].clone() } else { - NodeInput::value(TaggedValue::Bool(TypesettingConfig::default().max_height.is_some()), false) + NodeInput::value(TaggedValue::F64(0.), false) }, network_path, ); @@ -1193,7 +1194,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], if inputs_count >= 10 { old_inputs[9].clone() } else { - NodeInput::value(TaggedValue::F64(TypesettingConfig::default().max_height.unwrap_or(100.)), false) + NodeInput::value(TaggedValue::TextAlign(TextAlign::Left), false) }, network_path, ); @@ -1202,28 +1203,52 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], if inputs_count >= 11 { old_inputs[10].clone() } else { - NodeInput::value(TaggedValue::F64(TypesettingConfig::default().tilt), false) + NodeInput::value(TaggedValue::Bool(false), false) }, network_path, ); + inputs_count = 11 + } + + // https://github.com/GraphiteEditor/Graphite/pull/3643 + if reference == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER) && inputs_count == 11 { + log::debug!("second upgrade"); + let mut template: NodeTemplate = resolve_document_node_type(&reference)?.default_node_template(); + document.network_interface.replace_implementation(node_id, network_path, &mut template); + let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut template)?; + + document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); + document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); + document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path); + document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[3].clone(), network_path); + document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[4].clone(), network_path); + document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[5].clone(), network_path); + let Some(TaggedValue::OptionalF64(old_max_width)) = old_inputs[6].as_value() else { + return None; + }; + document + .network_interface + .set_input(&InputConnector::node(*node_id, 6), NodeInput::value(TaggedValue::Bool(old_max_width.is_some()), false), network_path); document.network_interface.set_input( - &InputConnector::node(*node_id, 11), - if inputs_count >= 12 { - old_inputs[11].clone() - } else { - NodeInput::value(TaggedValue::TextAlign(TextAlign::default()), false) - }, + &InputConnector::node(*node_id, 7), + NodeInput::value(TaggedValue::F64(old_max_width.unwrap_or_default()), false), network_path, ); + + let Some(TaggedValue::OptionalF64(old_max_height)) = old_inputs[7].as_value() else { + return None; + }; + document + .network_interface + .set_input(&InputConnector::node(*node_id, 8), NodeInput::value(TaggedValue::Bool(old_max_height.is_some()), false), network_path); document.network_interface.set_input( - &InputConnector::node(*node_id, 12), - if inputs_count >= 13 { - old_inputs[12].clone() - } else { - NodeInput::value(TaggedValue::Bool(false), false) - }, + &InputConnector::node(*node_id, 9), + NodeInput::value(TaggedValue::F64(old_max_height.unwrap_or_default()), false), network_path, ); + document.network_interface.set_input(&InputConnector::node(*node_id, 10), old_inputs[8].clone(), network_path); + document.network_interface.set_input(&InputConnector::node(*node_id, 11), old_inputs[9].clone(), network_path); + document.network_interface.set_input(&InputConnector::node(*node_id, 12), old_inputs[10].clone(), network_path); } // Upgrade Sine, Cosine, and Tangent nodes to include a boolean input for whether the output should be in radians, which was previously the only option but is now not the default diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 72e81f874d..099ffbee25 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -173,6 +173,7 @@ tagged_value! { U64(u64), Bool(bool), String(String), + OptionalF64(Option), ColorNotInTable(Color), // ======================== // LISTS OF PRIMITIVE TYPES From 9ae7f5dbcaaa31cf619f4a4e4a1b2a28190c7ae3 Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 15 Jan 2026 20:31:40 -0800 Subject: [PATCH 4/7] Remove OptionalF64 --- .../messages/portfolio/document_migration.rs | 53 ++++++++++++++++--- node-graph/graph-craft/src/document/value.rs | 1 - 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index 275481448a..b344a91807 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -27,6 +27,7 @@ const TEXT_REPLACEMENTS: &[(&str, &str)] = &[ "core::option::Option>", ), ("graphene_core::transform::Footprint", "graphene_core::transform::Footprint"), + ("\"OptionalF64\":", "\"F64\":"), ]; pub struct NodeReplacement<'a> { @@ -961,11 +962,48 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[ const REPLACEMENTS: &[(&str, &str)] = &[]; pub fn document_migration_string_preprocessing(document_serialized_content: String) -> String { + let document_serialized_content = replace_optional_f64_null(&document_serialized_content); + TEXT_REPLACEMENTS .iter() .fold(document_serialized_content, |document_serialized_content, (old, new)| document_serialized_content.replace(old, new)) } +fn replace_optional_f64_null(input: &str) -> String { + let mut result = String::new(); + let mut last_end = 0; + let key = "\"OptionalF64\":"; + + for (start, _) in input.match_indices(key) { + let search_start = start + key.len(); + if search_start >= input.len() { + continue; + } + + let mut after_key_start = search_start; + for (i, c) in input[search_start..].char_indices() { + if !c.is_whitespace() { + after_key_start = search_start + i; + break; + } + // If we reach the end and it's all whitespace, update after_key_start + if search_start + i + c.len_utf8() == input.len() { + after_key_start = input.len(); + } + } + + if input[after_key_start..].starts_with("null") { + result.push_str(&input[last_end..start]); + result.push_str(key); + result.push_str("0.0"); + last_end = after_key_start + "null".len(); + } + } + + result.push_str(&input[last_end..]); + result +} + pub fn document_migration_reset_node_definition(document_serialized_content: &str) -> bool { // Upgrade a document being opened to use fresh copies of all nodes if document_serialized_content.contains("node_output_index") { @@ -1135,7 +1173,6 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], // Upgrade Text node to include line height and character spacing, which were previously hardcoded to 1, from https://github.com/GraphiteEditor/Graphite/pull/2016 if reference == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER) && inputs_count == 8 { - log::debug!("first upgrade"); let mut template: NodeTemplate = resolve_document_node_type(&reference)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut template)?; @@ -1212,7 +1249,6 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], // https://github.com/GraphiteEditor/Graphite/pull/3643 if reference == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER) && inputs_count == 11 { - log::debug!("second upgrade"); let mut template: NodeTemplate = resolve_document_node_type(&reference)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut template)?; @@ -1223,27 +1259,28 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[3].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[4].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[5].clone(), network_path); - let Some(TaggedValue::OptionalF64(old_max_width)) = old_inputs[6].as_value() else { + let Some(&TaggedValue::F64(old_max_width)) = old_inputs[6].as_value() else { return None; }; document .network_interface - .set_input(&InputConnector::node(*node_id, 6), NodeInput::value(TaggedValue::Bool(old_max_width.is_some()), false), network_path); + .set_input(&InputConnector::node(*node_id, 6), NodeInput::value(TaggedValue::Bool(old_max_width != 0.), false), network_path); document.network_interface.set_input( &InputConnector::node(*node_id, 7), - NodeInput::value(TaggedValue::F64(old_max_width.unwrap_or_default()), false), + NodeInput::value(TaggedValue::F64(if old_max_width == 0. { 100. } else { old_max_width }), false), network_path, ); - let Some(TaggedValue::OptionalF64(old_max_height)) = old_inputs[7].as_value() else { + let Some(&TaggedValue::F64(old_max_height)) = old_inputs[7].as_value() else { return None; }; + document .network_interface - .set_input(&InputConnector::node(*node_id, 8), NodeInput::value(TaggedValue::Bool(old_max_height.is_some()), false), network_path); + .set_input(&InputConnector::node(*node_id, 8), NodeInput::value(TaggedValue::Bool(old_max_height != 0.), false), network_path); document.network_interface.set_input( &InputConnector::node(*node_id, 9), - NodeInput::value(TaggedValue::F64(old_max_height.unwrap_or_default()), false), + NodeInput::value(TaggedValue::F64(if old_max_height == 0. { 100. } else { old_max_height }), false), network_path, ); document.network_interface.set_input(&InputConnector::node(*node_id, 10), old_inputs[8].clone(), network_path); diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 099ffbee25..72e81f874d 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -173,7 +173,6 @@ tagged_value! { U64(u64), Bool(bool), String(String), - OptionalF64(Option), ColorNotInTable(Color), // ======================== // LISTS OF PRIMITIVE TYPES From 85e2aeb83b3684f23483e09cfdd5fc0426d15d1c Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 15 Jan 2026 21:28:45 -0800 Subject: [PATCH 5/7] Custom attributes for optional f64 widget --- .../node_graph/document_node_definitions.rs | 30 ++++++++++++++++++- node-graph/nodes/gstd/src/text.rs | 2 ++ 2 files changed, 31 insertions(+), 1 deletion(-) 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 d23325f7d8..9a17367c60 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 @@ -2200,11 +2200,39 @@ fn static_input_properties() -> InputProperties { }), ); map.insert( + // The custom number input settings are only available on proto nodes "optional_f64".to_string(), Box::new(|node_id, index, context| { + let node_metadata = registry::NODE_METADATA.lock().unwrap(); + let mut number_input = NumberInput::default(); + if let Some(field) = context + .network_interface + .implementation(&node_id, context.selection_network_path) + .and_then(|implementation| if let DocumentNodeImplementation::ProtoNode(id) = implementation { Some(id) } else { None }) + .and_then(|proto_node_identifier| node_metadata.get(proto_node_identifier)) + .and_then(|metadata| metadata.fields.get(index)) + { + if let Some(unit) = field.unit { + number_input = number_input.unit(unit); + } + if let Some(number_min) = field.number_min { + number_input = number_input.min(number_min); + } + if let Some(number_max) = field.number_max { + number_input = number_input.max(number_max); + } + if let Some((range_min, range_max)) = field.number_mode_range { + number_input = number_input.range_min(Some(range_min)); + number_input = number_input.range_max(Some(range_max)); + } + number_input = number_input.is_integer(false); + if let Some(number_step) = field.number_step { + number_input = number_input.step(number_step); + } + }; Ok(vec![LayoutGroup::Row { // The bool input must be directly above the number - widgets: node_properties::optional_f64_widget(ParameterWidgetsInfo::new(node_id, index, false, context), index - 1, NumberInput::default()), + widgets: node_properties::optional_f64_widget(ParameterWidgetsInfo::new(node_id, index, false, context), index - 1, number_input), }]) }), ); diff --git a/node-graph/nodes/gstd/src/text.rs b/node-graph/nodes/gstd/src/text.rs index 84cba91a9b..4cb280ba30 100644 --- a/node-graph/nodes/gstd/src/text.rs +++ b/node-graph/nodes/gstd/src/text.rs @@ -39,6 +39,7 @@ fn text<'i: 'n>( has_max_width: bool, /// The maximum width that the text block can occupy before wrapping to a new line. Otherwise, lines do not wrap. #[unit(" px")] + #[hard_min(1.)] #[widget(ParsedWidgetOverride::Custom = "optional_f64")] max_width: f64, /// Whether the *Max Height* property is enabled so that lines beyond it are not drawn. @@ -46,6 +47,7 @@ fn text<'i: 'n>( has_max_height: bool, /// The maximum height that the text block can occupy. Excess lines are not drawn. #[unit(" px")] + #[hard_min(1.)] #[widget(ParsedWidgetOverride::Custom = "optional_f64")] max_height: f64, /// The angle of faux italic slant applied to each glyph. From 9b255f7538ae8551ef8d3b7a068db83b8e1fc766 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 15 Jan 2026 21:57:18 -0800 Subject: [PATCH 6/7] Code review --- .../node_graph/document_node_definitions.rs | 2 +- .../document/node_graph/node_properties.rs | 3 +- .../messages/portfolio/document_migration.rs | 41 ++++++++-------- .../graph_modification_utils.rs | 48 ++++++++++++++----- .../messages/tool/tool_messages/text_tool.rs | 10 ++-- 5 files changed, 64 insertions(+), 40 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index 9a17367c60..bb90f2c946 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 @@ -2231,7 +2231,7 @@ fn static_input_properties() -> InputProperties { } }; Ok(vec![LayoutGroup::Row { - // The bool input must be directly above the number + // NOTE: The bool input MUST be at the input index directly before the f64 input! widgets: node_properties::optional_f64_widget(ParameterWidgetsInfo::new(node_id, index, false, context), index - 1, number_input), }]) }), 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 ae1f65c569..c644a34f14 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -939,8 +939,7 @@ pub fn progression_widget(parameter_widgets_info: ParameterWidgetsInfo, number_p widgets } -/// ParameterWidgetsInfo is for the number -/// Bool index is the input index of the bool parameter displayed in the checkbox +/// `parameter_widgets_info` is for the f64 parameter. `bool_input_index` is the input index of the bool parameter for the checkbox. pub fn optional_f64_widget(parameter_widgets_info: ParameterWidgetsInfo, bool_input_index: usize, number_props: NumberInput) -> Vec { let ParameterWidgetsInfo { document_node, diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index b344a91807..aa4ccaa174 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -1204,7 +1204,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], if inputs_count >= 7 { old_inputs[6].clone() } else { - NodeInput::value(TaggedValue::Bool(TypesettingConfig::default().max_width.is_some()), false) + NodeInput::value(TaggedValue::F64(TypesettingConfig::default().max_width.unwrap_or_default()), false) }, network_path, ); @@ -1213,7 +1213,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], if inputs_count >= 8 { old_inputs[7].clone() } else { - NodeInput::value(TaggedValue::F64(TypesettingConfig::default().max_width.unwrap_or(100.)), false) + NodeInput::value(TaggedValue::F64(TypesettingConfig::default().max_width.unwrap_or_default()), false) }, network_path, ); @@ -1222,7 +1222,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], if inputs_count >= 9 { old_inputs[8].clone() } else { - NodeInput::value(TaggedValue::F64(0.), false) + NodeInput::value(TaggedValue::F64(TypesettingConfig::default().tilt), false) }, network_path, ); @@ -1231,7 +1231,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], if inputs_count >= 10 { old_inputs[9].clone() } else { - NodeInput::value(TaggedValue::TextAlign(TextAlign::Left), false) + NodeInput::value(TaggedValue::TextAlign(TextAlign::default()), false) }, network_path, ); @@ -1247,21 +1247,21 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], inputs_count = 11 } + // Insert bool parameters for `has_max_width` and `has_max_height`: // https://github.com/GraphiteEditor/Graphite/pull/3643 if reference == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER) && inputs_count == 11 { let mut template: NodeTemplate = resolve_document_node_type(&reference)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut template)?; - document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); - document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); - document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path); - document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[3].clone(), network_path); - document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[4].clone(), network_path); - document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[5].clone(), network_path); - let Some(&TaggedValue::F64(old_max_width)) = old_inputs[6].as_value() else { - return None; - }; + // Copy over old inputs + #[allow(clippy::needless_range_loop)] + for i in 0..=5 { + document.network_interface.set_input(&InputConnector::node(*node_id, i), old_inputs[i].clone(), network_path); + } + + // Max Width + let Some(&TaggedValue::F64(old_max_width)) = old_inputs[6].as_value() else { return None }; document .network_interface .set_input(&InputConnector::node(*node_id, 6), NodeInput::value(TaggedValue::Bool(old_max_width != 0.), false), network_path); @@ -1271,10 +1271,8 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], network_path, ); - let Some(&TaggedValue::F64(old_max_height)) = old_inputs[7].as_value() else { - return None; - }; - + // Max Height + let Some(&TaggedValue::F64(old_max_height)) = old_inputs[7].as_value() else { return None }; document .network_interface .set_input(&InputConnector::node(*node_id, 8), NodeInput::value(TaggedValue::Bool(old_max_height != 0.), false), network_path); @@ -1283,9 +1281,12 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], NodeInput::value(TaggedValue::F64(if old_max_height == 0. { 100. } else { old_max_height }), false), network_path, ); - document.network_interface.set_input(&InputConnector::node(*node_id, 10), old_inputs[8].clone(), network_path); - document.network_interface.set_input(&InputConnector::node(*node_id, 11), old_inputs[9].clone(), network_path); - document.network_interface.set_input(&InputConnector::node(*node_id, 12), old_inputs[10].clone(), network_path); + + // Copy over old inputs + #[allow(clippy::needless_range_loop)] + for i in 10..=12 { + document.network_interface.set_input(&InputConnector::node(*node_id, i), old_inputs[i - 2].clone(), network_path); + } } // Upgrade Sine, Cosine, and Tangent nodes to include a boolean input for whether the output should be in radians, which was previously the only option but is now not the default diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index a8c2fb6e2d..a0d4cf1dbd 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -388,18 +388,42 @@ pub fn get_grid_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkIn pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<(&String, &Font, TypesettingConfig, bool)> { let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER))?; - let Some(TaggedValue::String(text)) = &inputs[1].as_value() else { return None }; - let Some(TaggedValue::Font(font)) = &inputs[2].as_value() else { return None }; - let Some(&TaggedValue::F64(font_size)) = inputs[3].as_value() else { return None }; - let Some(&TaggedValue::F64(line_height_ratio)) = inputs[4].as_value() else { return None }; - let Some(&TaggedValue::F64(character_spacing)) = inputs[5].as_value() else { return None }; - let Some(&TaggedValue::Bool(has_max_width)) = inputs[6].as_value() else { return None }; - let Some(&TaggedValue::F64(max_width)) = inputs[7].as_value() else { return None }; - let Some(&TaggedValue::Bool(has_max_height)) = inputs[8].as_value() else { return None }; - let Some(&TaggedValue::F64(max_height)) = inputs[9].as_value() else { return None }; - let Some(&TaggedValue::F64(tilt)) = inputs[10].as_value() else { return None }; - let Some(&TaggedValue::TextAlign(align)) = inputs[11].as_value() else { return None }; - let Some(&TaggedValue::Bool(per_glyph_instances)) = inputs[12].as_value() else { return None }; + let Some(TaggedValue::String(text)) = &inputs[graphene_std::text::text::TextInput::INDEX].as_value() else { + return None; + }; + let Some(TaggedValue::Font(font)) = &inputs[graphene_std::text::text::FontInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::F64(font_size)) = inputs[graphene_std::text::text::SizeInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::F64(line_height_ratio)) = inputs[graphene_std::text::text::LineHeightInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::F64(character_spacing)) = inputs[graphene_std::text::text::CharacterSpacingInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::Bool(has_max_width)) = inputs[graphene_std::text::text::HasMaxWidthInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::F64(max_width)) = inputs[graphene_std::text::text::MaxWidthInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::Bool(has_max_height)) = inputs[graphene_std::text::text::HasMaxHeightInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::F64(max_height)) = inputs[graphene_std::text::text::MaxHeightInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::F64(tilt)) = inputs[graphene_std::text::text::TiltInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::TextAlign(align)) = inputs[graphene_std::text::text::AlignInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::Bool(per_glyph_instances)) = inputs[graphene_std::text::text::SeparateGlyphElementsInput::INDEX].as_value() else { + return None; + }; let typesetting = TypesettingConfig { font_size, diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index 75033194de..1bd12b61ac 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -18,10 +18,10 @@ use crate::messages::tool::common_functionality::utility_functions::text_boundin use crate::messages::tool::utility_types::ToolRefreshOptions; use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput}; -use graphene_std::Color; use graphene_std::renderer::Quad; use graphene_std::text::{Font, FontCache, TextAlign, TypesettingConfig, lines_clipping}; use graphene_std::vector::style::Fill; +use graphene_std::{Color, NodeInputDecleration}; #[derive(Default, ExtractField)] pub struct TextTool { @@ -802,19 +802,19 @@ impl Fsm for TextToolFsmState { // TODO: Don't set both max_width and max_height to true at the same time, only do one based on which edge is being dragged (or both if a corner is being dragged) responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 6), + input_connector: InputConnector::node(node_id, graphene_std::text::text::HasMaxWidthInput::INDEX), input: NodeInput::value(TaggedValue::Bool(true), false), }); responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 7), + input_connector: InputConnector::node(node_id, graphene_std::text::text::MaxWidthInput::INDEX), input: NodeInput::value(TaggedValue::F64(size_layer.x), false), }); responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 8), + input_connector: InputConnector::node(node_id, graphene_std::text::text::HasMaxHeightInput::INDEX), input: NodeInput::value(TaggedValue::Bool(true), false), }); responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 9), + input_connector: InputConnector::node(node_id, graphene_std::text::text::MaxHeightInput::INDEX), input: NodeInput::value(TaggedValue::F64(size_layer.y), false), }); responses.add(GraphOperationMessage::TransformSet { From 7f6be79f3b414a330d03e459d6f8116c693a995d Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 15 Jan 2026 21:58:15 -0800 Subject: [PATCH 7/7] Move comments to another PR --- node-graph/nodes/gstd/src/text.rs | 32 +++++-------------------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/node-graph/nodes/gstd/src/text.rs b/node-graph/nodes/gstd/src/text.rs index 4cb280ba30..ff8bf0e8e3 100644 --- a/node-graph/nodes/gstd/src/text.rs +++ b/node-graph/nodes/gstd/src/text.rs @@ -3,63 +3,41 @@ use graph_craft::wasm_application_io::WasmEditorApi; use graphic_types::Vector; pub use text_nodes::*; -/// Draws a text string as vector geometry with a choice of font and styling. #[node_macro::node(category("Text"))] fn text<'i: 'n>( _: impl Ctx, - /// The Graphite editor's source for global font resources. - #[scope("editor-api")] - editor_resources: &'i WasmEditorApi, - /// The text content to be drawn. + #[scope("editor-api")] editor_resources: &'i WasmEditorApi, #[widget(ParsedWidgetOverride::Custom = "text_area")] #[default("Lorem ipsum")] text: String, - /// The typeface used to draw the text. - #[widget(ParsedWidgetOverride::Custom = "text_font")] - font: Font, - /// The font size used to draw the text. + #[widget(ParsedWidgetOverride::Custom = "text_font")] font: Font, #[unit(" px")] #[default(24.)] #[hard_min(1.)] size: f64, - /// The line height ratio, relative to the font size. Each line is drawn lower than its previous line by the distance of *Size* × *Line Height*. - /// - /// 0 means all lines overlap. 1 means all lines are spaced by just the font size. 1.2 is a common default for readable text. 2 means double-spaced text. #[unit("x")] #[hard_min(0.)] #[step(0.1)] #[default(1.2)] line_height: f64, - /// Additional spacing, in pixels, added between each character. #[unit(" px")] #[step(0.1)] character_spacing: f64, - /// Whether the *Max Width* property is enabled so that lines can wrap to fit its specified block width. - #[widget(ParsedWidgetOverride::Hidden)] - has_max_width: bool, - /// The maximum width that the text block can occupy before wrapping to a new line. Otherwise, lines do not wrap. + #[widget(ParsedWidgetOverride::Hidden)] has_max_width: bool, #[unit(" px")] #[hard_min(1.)] #[widget(ParsedWidgetOverride::Custom = "optional_f64")] max_width: f64, - /// Whether the *Max Height* property is enabled so that lines beyond it are not drawn. - #[widget(ParsedWidgetOverride::Hidden)] - has_max_height: bool, - /// The maximum height that the text block can occupy. Excess lines are not drawn. + #[widget(ParsedWidgetOverride::Hidden)] has_max_height: bool, #[unit(" px")] #[hard_min(1.)] #[widget(ParsedWidgetOverride::Custom = "optional_f64")] max_height: f64, - /// The angle of faux italic slant applied to each glyph. #[unit("°")] #[hard_min(-85.)] #[hard_max(85.)] tilt: f64, - /// The horizontal alignment of each line of text within its surrounding box. - /// To have an effect on a single line of text, *Max Width* must be set. - #[widget(ParsedWidgetOverride::Custom = "text_align")] - align: TextAlign, - /// Whether to split every letterform into its own vector path element. Otherwise, a single compound path is produced. + #[widget(ParsedWidgetOverride::Custom = "text_align")] align: TextAlign, separate_glyph_elements: bool, ) -> Table { let typesetting = TypesettingConfig {