From aa789e3de6bdb90ca693c0c9863d95cb421f6deb Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 27 Apr 2026 19:00:13 -0700 Subject: [PATCH 1/7] Improve the Data panel with more type-specific detail pages --- .../document/data_panel/data_panel_message.rs | 3 + .../data_panel/data_panel_message_handler.rs | 204 ++++++++++++------ .../node_graph/node_graph_message_handler.rs | 2 + 3 files changed, 148 insertions(+), 61 deletions(-) diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message.rs index 57a93ed2b3..88f42bd78f 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message.rs @@ -10,6 +10,9 @@ pub enum DataPanelMessage { inspect_result: InspectResult, }, ClearLayout, + /// Re-render the existing layout against the latest network interface state. Use this when node metadata + /// (display name, visibility, locked, etc.) changes but the introspected output value hasn't. + Refresh, PushToElementPath { step: PathStep, diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index cf366a1bfe..83c303a586 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -51,6 +51,12 @@ impl MessageHandler> for DataPanel self.active_vector_table_tab = VectorTableTab::default(); self.update_layout(responses, context); } + DataPanelMessage::Refresh => { + // Re-render against the current network_interface without disturbing introspected_data or the breadcrumb path. + if self.introspected_data.is_some() { + self.update_layout(responses, context); + } + } DataPanelMessage::PushToElementPath { step } => { self.element_path.push(step); @@ -80,6 +86,7 @@ impl DataPanelMessageHandler { let mut layout_data = LayoutData { current_depth: 0, desired_path: &mut self.element_path, + network_interface: &*network_interface, breadcrumbs: Vec::new(), vector_table_tab: self.active_vector_table_tab, }; @@ -144,6 +151,7 @@ impl DataPanelMessageHandler { struct LayoutData<'a> { current_depth: usize, desired_path: &'a mut Vec, + network_interface: &'a NodeNetworkInterface, breadcrumbs: Vec, vector_table_tab: VectorTableTab, } @@ -203,10 +211,12 @@ trait TableRowLayout { } /// Renders this value as a single inline widget inside a row of a Vec/Table. /// `target` is the [`PathStep`] to push when the cell is clicked to drill into the value. + /// `data` provides shared context (notably `network_interface`) for types whose label or content + /// depends on lookup beyond their own value (e.g. `NodeId` resolving a node's display name). /// The default is a button labeled with `identifier()`. Types whose values are best shown /// inline (colors, transforms, primitives, etc.) override this to ignore `target` and /// return a richer non-navigating widget. - fn cell_widget(&self, target: PathStep) -> WidgetInstance { + fn cell_widget(&self, target: PathStep, _data: &LayoutData) -> WidgetInstance { TextButton::new(self.identifier()) .on_update(move |_| DataPanelMessage::PushToElementPath { step: target.clone() }.into()) .narrow(true) @@ -258,10 +268,10 @@ impl TableRowLayout for Table { let mut rows = (0..self.len()) .map(|index| { let element = self.element(index).unwrap(); - let mut cells = vec![TextLabel::new(format!("{index}")).narrow(true).widget_instance(), element.cell_widget(PathStep::Element(index))]; + let mut cells = vec![TextLabel::new(format!("{index}")).narrow(true).widget_instance(), element.cell_widget(PathStep::Element(index), data)]; for key in &attribute_keys { let target = PathStep::Attribute { row: index, key: key.clone() }; - let widget = self.attribute_any(key, index).and_then(|any| dispatch_cell_widget(any, target)).unwrap_or_else(|| { + let widget = self.attribute_any(key, index).and_then(|any| dispatch_cell_widget(any, target, data)).unwrap_or_else(|| { let text = self.attribute_display_value(key, index, |_| None).unwrap_or_else(|| "-".to_string()); TextLabel::new(text).narrow(true).widget_instance() }); @@ -531,7 +541,7 @@ impl TableRowLayout for Color { fn identifier(&self) -> String { format!("Color (#{})", self.to_gamma_srgb().to_rgba_hex_srgb()) } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { + fn cell_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance { ColorInput::new(FillChoice::Solid(*self)) .disabled(true) .menu_direction(Some(MenuDirection::Top)) @@ -539,7 +549,7 @@ impl TableRowLayout for Color { .widget_instance() } fn element_page(&self, _data: &mut LayoutData) -> Vec { - let widgets = vec![self.cell_widget(PathStep::Element(0))]; + let widgets = vec![self.cell_widget(PathStep::Element(0), _data)]; vec![LayoutGroup::row(widgets)] } } @@ -551,7 +561,7 @@ impl TableRowLayout for GradientStops { fn identifier(&self) -> String { format!("Gradient ({} stops)", self.len()) } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { + fn cell_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance { ColorInput::new(FillChoice::Gradient(self.clone())) .menu_direction(Some(MenuDirection::Top)) .disabled(true) @@ -559,7 +569,7 @@ impl TableRowLayout for GradientStops { .widget_instance() } fn element_page(&self, _data: &mut LayoutData) -> Vec { - let widgets = vec![self.cell_widget(PathStep::Element(0))]; + let widgets = vec![self.cell_widget(PathStep::Element(0), _data)]; vec![LayoutGroup::row(widgets)] } } @@ -569,13 +579,13 @@ impl TableRowLayout for f64 { "Number (f64)" } fn identifier(&self) -> String { - "Number (f64)".to_string() - } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { - NumberInput::new(Some(*self)).disabled(true).max_width(220).display_decimal_places(20).widget_instance() + format!("{self}") } + // Cells fall back to the default drill-in button (labeled with the value via `identifier`); the leaf page shows the rich `NumberInput`. fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + vec![LayoutGroup::row(vec![ + NumberInput::new(Some(*self)).disabled(true).max_width(220).display_decimal_places(20).widget_instance(), + ])] } } @@ -586,11 +596,9 @@ impl TableRowLayout for u8 { fn identifier(&self) -> String { format!("{self:02X}") } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { - TextLabel::new(self.identifier()).narrow(true).widget_instance() - } + // Cells fall back to the default drill-in button (labeled with the hex value via `identifier`); the leaf page shows the same hex value as a label. fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + vec![LayoutGroup::row(vec![TextLabel::new(self.identifier()).widget_instance()])] } } @@ -599,13 +607,13 @@ impl TableRowLayout for u32 { "Number (u32)" } fn identifier(&self) -> String { - "Number (u32)".to_string() - } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { - NumberInput::new(Some(*self as f64)).disabled(true).max_width(220).display_decimal_places(20).widget_instance() + format!("{self}") } + // Cells fall back to the default drill-in button (labeled with the value via `identifier`); the leaf page shows the rich `NumberInput`. fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + vec![LayoutGroup::row(vec![ + NumberInput::new(Some(*self as f64)).disabled(true).max_width(220).display_decimal_places(20).widget_instance(), + ])] } } @@ -614,14 +622,14 @@ impl TableRowLayout for u64 { "Number (u64)" } fn identifier(&self) -> String { - "Number (u64)".to_string() + format!("{self}") } + // Cells fall back to the default drill-in button (labeled with the value via `identifier`); the leaf page shows the rich `NumberInput`. // TODO: Make this robust for large u64 values that don't fit in f64 (above roughly 2^53). Perhaps using a bigint kind of approach through the widget's data flow. - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { - NumberInput::new(Some(*self as f64)).disabled(true).max_width(220).display_decimal_places(20).widget_instance() - } fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + vec![LayoutGroup::row(vec![ + NumberInput::new(Some(*self as f64)).disabled(true).max_width(220).display_decimal_places(20).widget_instance(), + ])] } } @@ -632,11 +640,11 @@ impl TableRowLayout for bool { fn identifier(&self) -> String { "Bool".to_string() } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { + fn cell_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance { TextLabel::new(self.to_string()).narrow(true).widget_instance() } fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0), _data)])] } } @@ -653,9 +661,7 @@ impl TableRowLayout for String { format!("\"{}\"", first_line) } } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { - TextLabel::new(self.identifier()).narrow(true).widget_instance() - } + // Cells fall back to the default drill-in button (labeled with the truncated quoted preview via `identifier`); the leaf page shows the full multi-line text in a `TextAreaInput`. fn element_page(&self, _data: &mut LayoutData) -> Vec { vec![LayoutGroup::row(vec![TextAreaInput::new(self.to_string()).monospace(true).disabled(true).widget_instance()])] } @@ -668,11 +674,11 @@ impl TableRowLayout for Option { fn identifier(&self) -> String { "Option".to_string() } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { + fn cell_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance { TextLabel::new(format!("{self:?}")).narrow(true).widget_instance() } fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0), _data)])] } } @@ -683,11 +689,11 @@ impl TableRowLayout for DVec2 { fn identifier(&self) -> String { "Vec2".to_string() } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { + fn cell_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance { TextLabel::new(format_dvec2(*self)).narrow(true).widget_instance() } fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0), _data)])] } } @@ -698,11 +704,11 @@ impl TableRowLayout for Vec2 { fn identifier(&self) -> String { "Vec2".to_string() } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { + fn cell_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance { TextLabel::new(format_dvec2(DVec2::new(self.x as f64, self.y as f64))).narrow(true).widget_instance() } fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0), _data)])] } } @@ -713,11 +719,11 @@ impl TableRowLayout for DAffine2 { fn identifier(&self) -> String { "Transform".to_string() } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { + fn cell_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance { TextLabel::new(format_transform_matrix(*self)).narrow(true).widget_instance() } fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0), _data)])] } } @@ -728,12 +734,12 @@ impl TableRowLayout for Affine2 { fn identifier(&self) -> String { "Transform".to_string() } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { + fn cell_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance { let matrix = DAffine2::from_cols_array(&self.to_cols_array().map(|x| x as f64)); TextLabel::new(format_transform_matrix(matrix)).narrow(true).widget_instance() } fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0), _data)])] } } @@ -744,11 +750,22 @@ impl TableRowLayout for AlphaBlending { fn identifier(&self) -> String { format_alpha_blending(*self) } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { + fn cell_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance { TextLabel::new(format_alpha_blending(*self)).narrow(true).widget_instance() } fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0), _data)])] + } +} + +/// Resolves the cell/breadcrumb label for a `NodeId` from the root network's metadata, falling back +/// to "Node {id}" if the node isn't present (e.g. an ID that no longer maps to a real node). +fn node_id_display_label(node_id: NodeId, network_interface: &NodeNetworkInterface) -> String { + let network_path: &[NodeId] = &[]; + if network_interface.node_metadata(&node_id, network_path).is_some() { + network_interface.display_name(&node_id, network_path) + } else { + format!("Node {node_id}") } } @@ -759,16 +776,70 @@ impl TableRowLayout for NodeId { fn identifier(&self) -> String { format!("Node {self}") } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { - let node_id = *self; - TextButton::new("Go to Node") - .tooltip_description("Click to select the node with this ID in the graph.") - .on_update(move |_| NodeGraphMessage::SelectedNodesSet { nodes: vec![node_id] }.into()) + // Override so the breadcrumb uses the same resolved display name as the cell button, instead of the bare-ID fallback `identifier()` returns. + fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec { + data.breadcrumbs.push(node_id_display_label(*self, data.network_interface)); + self.element_page(data) + } + // Cell label resolves the node's display name via the network interface (looked up at the root network) so the + // button reads as the name shown in the Node Graph / Layers panels. Falls back to "Node {id}" if the lookup misses. + fn cell_widget(&self, target: PathStep, data: &LayoutData) -> WidgetInstance { + let label = node_id_display_label(*self, data.network_interface); + TextButton::new(label) + .on_update(move |_| DataPanelMessage::PushToElementPath { step: target.clone() }.into()) .narrow(true) .widget_instance() } - fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + // The leaf page shows the node's kind, name, lock/visibility toggles, and a "Make Selected" action button. + fn element_page(&self, data: &mut LayoutData) -> Vec { + let node_id = *self; + // Layer NodeIds (e.g. via the `editor:layer` attribute) live at the root network; if the lookup misses we just show the placeholder name. + let network_path: &[NodeId] = &[]; + let known = data.network_interface.node_metadata(&node_id, network_path).is_some(); + let name = if known { + data.network_interface.display_name(&node_id, network_path) + } else { + "(node not found in root network)".to_string() + }; + let kind_widget = if known { + let icon = if data.network_interface.is_layer(&node_id, network_path) { "Layer" } else { "Node" }; + IconLabel::new(icon).widget_instance() + } else { + TextLabel::new("-").widget_instance() + }; + + let mut header = vec![kind_widget, Separator::new(SeparatorStyle::Related).widget_instance(), TextLabel::new(name).widget_instance()]; + + if known { + let is_locked = data.network_interface.is_locked(&node_id, network_path); + let is_visible = data.network_interface.is_visible(&node_id, network_path); + + header.push(Separator::new(SeparatorStyle::Unrelated).widget_instance()); + header.push( + IconButton::new(if is_locked { "PadlockLocked" } else { "PadlockUnlocked" }, 24) + .hover_icon(if is_locked { "PadlockUnlocked" } else { "PadlockLocked" }) + .tooltip_label(if is_locked { "Unlock" } else { "Lock" }) + .on_update(move |_| NodeGraphMessage::ToggleLocked { node_id }.into()) + .widget_instance(), + ); + header.push( + IconButton::new(if is_visible { "EyeVisible" } else { "EyeHidden" }, 24) + .hover_icon(if is_visible { "EyeHide" } else { "EyeShow" }) + .tooltip_label(if is_visible { "Hide" } else { "Show" }) + .on_update(move |_| NodeGraphMessage::ToggleVisibility { node_id }.into()) + .widget_instance(), + ); + } + + header.push(Separator::new(SeparatorStyle::Unrelated).widget_instance()); + header.push( + TextButton::new("Make Selected") + .tooltip_description("Click to select the node with this ID in the graph.") + .on_update(move |_| NodeGraphMessage::SelectedNodesSet { nodes: vec![node_id] }.into()) + .widget_instance(), + ); + + vec![LayoutGroup::row(header)] } } @@ -782,18 +853,29 @@ impl TableRowLayout for Option { None => "-".to_string(), } } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { - match *self { - Some(node_id) => TextButton::new("Go to Node") - .tooltip_description("Click to select the node with this ID in the graph.") - .on_update(move |_| NodeGraphMessage::SelectedNodesSet { nodes: vec![node_id] }.into()) - .narrow(true) - .widget_instance(), + // Cells defer to `NodeId`'s named cell button for `Some` (so the label reads as the node's display name), + // or render a plain "-" label for `None`. The leaf page likewise defers to `NodeId` for `Some`. + fn cell_widget(&self, target: PathStep, data: &LayoutData) -> WidgetInstance { + match self { + Some(node_id) => node_id.cell_widget(target, data), None => TextLabel::new("-").narrow(true).widget_instance(), } } - fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + // Defer to `NodeId`'s breadcrumb for `Some` so it stays in sync with the cell label; `None` shows just "-". + fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec { + match self { + Some(node_id) => node_id.layout_with_breadcrumb(data), + None => { + data.breadcrumbs.push("-".to_string()); + self.element_page(data) + } + } + } + fn element_page(&self, data: &mut LayoutData) -> Vec { + match self { + Some(node_id) => node_id.element_page(data), + None => vec![LayoutGroup::row(vec![TextLabel::new("-").widget_instance()])], + } } } @@ -843,12 +925,12 @@ macro_rules! known_table_row_types { /// Delegates to [`TableRowLayout::cell_widget`] so the same widget code is shared between /// element-column rendering and attribute-column rendering. Returns `None` for unrecognized types so the /// caller can fall back to a debug-formatted [`TextLabel`]. -fn dispatch_cell_widget(any: &dyn Any, target: PathStep) -> Option { +fn dispatch_cell_widget(any: &dyn Any, target: PathStep, data: &LayoutData) -> Option { macro_rules! check { ( $($ty:ty),* $(,)? ) => { $( if let Some(value) = any.downcast_ref::<$ty>() { - return Some(value.cell_widget(target)); + return Some(value.cell_widget(target, data)); } )* }; diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 0d8a4239d1..dfa3725b21 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -1831,6 +1831,7 @@ impl<'a> MessageHandler> for NodeG responses.add(DocumentMessage::RenderScrollbars); responses.add(NodeGraphMessage::SendGraph); responses.add(OverlaysMessage::Draw); // Redraw overlays to update artboard names + responses.add(DataPanelMessage::Refresh); } NodeGraphMessage::SetDisplayNameImpl { node_id, alias } => { network_interface.set_display_name(&node_id, alias, selection_network_path); @@ -1945,6 +1946,7 @@ impl<'a> MessageHandler> for NodeG responses.add(NodeGraphMessage::SendGraph); responses.add(PropertiesPanelMessage::Refresh); + responses.add(DataPanelMessage::Refresh); } NodeGraphMessage::UpdateBoxSelection => { if let Some((box_selection_start, _)) = self.box_selection_start { From 76645dafd6eeccf48c5d1b44ee36ea7074298816 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 28 Apr 2026 00:50:25 -0700 Subject: [PATCH 2/7] Add network_path to SetDisplayName so renames target any network depth --- .../new_document_dialog_message_handler.rs | 1 + .../data_panel/data_panel_message_handler.rs | 2 ++ .../portfolio/document/document_message_handler.rs | 2 ++ .../graph_operation_message_handler.rs | 4 ++++ .../document/node_graph/node_graph_message.rs | 4 ++++ .../node_graph/node_graph_message_handler.rs | 12 +++++++++--- frontend/wrapper/src/editor_wrapper.rs | 1 + 7 files changed, 23 insertions(+), 3 deletions(-) diff --git a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs index 9abd4ba5b7..4925eecc55 100644 --- a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs +++ b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs @@ -35,6 +35,7 @@ impl MessageHandler for NewDocumentDialogMessageHa }); responses.add(NodeGraphMessage::SetDisplayNameImpl { node_id, + network_path: Vec::new(), alias: "Background".to_string(), }); responses.add(NodeGraphMessage::SetLocked { node_id, locked: true }); diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index 83c303a586..26bcdc9daa 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -105,6 +105,7 @@ impl DataPanelMessageHandler { if let Some((node_id, parent_path)) = self.introspected_node_path.split_last() { let node_id = *node_id; let is_layer = network_interface.is_layer(&node_id, parent_path); + let parent_path_owned = parent_path.to_vec(); widgets.extend([ if is_layer { @@ -118,6 +119,7 @@ impl DataPanelMessageHandler { .on_update(move |text_input| { NodeGraphMessage::SetDisplayName { node_id, + network_path: parent_path_owned.clone(), alias: text_input.value.clone(), skip_adding_history_step: false, } diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 7414c7a5ff..dae0f1f48a 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -699,6 +699,7 @@ impl MessageHandler> for DocumentMes if let Some(name) = name { responses.add(NodeGraphMessage::SetDisplayName { node_id: layer.to_node(), + network_path: Vec::new(), alias: name, skip_adding_history_step: false, }); @@ -756,6 +757,7 @@ impl MessageHandler> for DocumentMes if let Some(name) = name { responses.add(NodeGraphMessage::SetDisplayName { node_id: layer.to_node(), + network_path: Vec::new(), alias: name, skip_adding_history_step: false, }); diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index 65e14a3f70..b77914d341 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -197,10 +197,12 @@ impl MessageHandler> for responses.add(NodeGraphMessage::SetDisplayNameImpl { node_id: id, + network_path: Vec::new(), alias: layer_alias.to_string(), }); responses.add(NodeGraphMessage::SetDisplayNameImpl { node_id: control_path_id, + network_path: Vec::new(), alias: path_alias.to_string(), }); } @@ -245,6 +247,7 @@ impl MessageHandler> for network_interface.move_layer_to_stack(layer, parent, insert_index, &[]); responses.add(NodeGraphMessage::SetDisplayNameImpl { node_id: id, + network_path: Vec::new(), alias: "Boolean Operation".to_string(), }); responses.add(NodeGraphMessage::RunDocumentGraph); @@ -343,6 +346,7 @@ impl MessageHandler> for responses.add(NodeGraphMessage::SetDisplayName { node_id, + network_path: Vec::new(), alias: network_interface.display_name(&artboard.to_node(), &[]), skip_adding_history_step: true, }); diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs index 4ec91d9981..c03c5a7500 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs @@ -157,11 +157,15 @@ pub enum NodeGraphMessage { }, SetDisplayName { node_id: NodeId, + /// The path to the network containing `node_id`. Empty for nodes at the root document network. + /// Lets the rename target a node at any nesting depth, independent of the current selection network. + network_path: Vec, alias: String, skip_adding_history_step: bool, }, SetDisplayNameImpl { node_id: NodeId, + network_path: Vec, alias: String, }, SetToNodeOrLayer { diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index dfa3725b21..da11918ac2 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -659,6 +659,7 @@ impl<'a> MessageHandler> for NodeG }); responses.add(NodeGraphMessage::SetDisplayNameImpl { node_id: encapsulating_node_id, + network_path: selection_network_path.to_vec(), alias: "Untitled Node".to_string(), }); @@ -1816,13 +1817,14 @@ impl<'a> MessageHandler> for NodeG } NodeGraphMessage::SetDisplayName { node_id, + network_path, alias, skip_adding_history_step, } => { if !skip_adding_history_step { responses.add(DocumentMessage::StartTransaction); } - responses.add(NodeGraphMessage::SetDisplayNameImpl { node_id, alias }); + responses.add(NodeGraphMessage::SetDisplayNameImpl { node_id, network_path, alias }); if !skip_adding_history_step { // Does not add a history step if the name was not changed responses.add(DocumentMessage::EndTransaction); @@ -1833,8 +1835,8 @@ impl<'a> MessageHandler> for NodeG responses.add(OverlaysMessage::Draw); // Redraw overlays to update artboard names responses.add(DataPanelMessage::Refresh); } - NodeGraphMessage::SetDisplayNameImpl { node_id, alias } => { - network_interface.set_display_name(&node_id, alias, selection_network_path); + NodeGraphMessage::SetDisplayNameImpl { node_id, network_path, alias } => { + network_interface.set_display_name(&node_id, alias, &network_path); } NodeGraphMessage::SetImportExportName { name, index } => { responses.add(DocumentMessage::StartTransaction); @@ -2399,6 +2401,7 @@ impl NodeGraphMessageHandler { let mut properties = Vec::new(); if let [node_id] = *nodes.as_slice() { + let network_path = context.selection_network_path.to_vec(); properties.push(LayoutGroup::row(vec![ Separator::new(SeparatorStyle::Related).widget_instance(), IconLabel::new("Node").tooltip_description("Name of the selected node.").widget_instance(), @@ -2408,6 +2411,7 @@ impl NodeGraphMessageHandler { .on_update(move |text_input| { NodeGraphMessage::SetDisplayName { node_id, + network_path: network_path.clone(), alias: text_input.value.clone(), skip_adding_history_step: false, } @@ -2470,6 +2474,7 @@ impl NodeGraphMessageHandler { return Vec::new(); } + let layer_network_path = context.selection_network_path.to_vec(); let mut layer_properties = vec![LayoutGroup::row(vec![ Separator::new(SeparatorStyle::Related).widget_instance(), IconLabel::new("Layer").tooltip_description("Name of the selected layer.").widget_instance(), @@ -2479,6 +2484,7 @@ impl NodeGraphMessageHandler { .on_update(move |text_input| { NodeGraphMessage::SetDisplayName { node_id: layer, + network_path: layer_network_path.clone(), alias: text_input.value.clone(), skip_adding_history_step: false, } diff --git a/frontend/wrapper/src/editor_wrapper.rs b/frontend/wrapper/src/editor_wrapper.rs index e9dd0edf6d..b953256c3e 100644 --- a/frontend/wrapper/src/editor_wrapper.rs +++ b/frontend/wrapper/src/editor_wrapper.rs @@ -775,6 +775,7 @@ impl EditorWrapper { let layer = LayerNodeIdentifier::new_unchecked(NodeId(id)); let message = NodeGraphMessage::SetDisplayName { node_id: layer.to_node(), + network_path: Vec::new(), alias: name, skip_adding_history_step: false, }; From b012e48375b499e52e3d1668fa79e0b522a5f08e Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 28 Apr 2026 00:56:42 -0700 Subject: [PATCH 3/7] Track nested layers via full editor:layer paths and rename parent_layer to path_of_subgraph --- .../data_panel/data_panel_message_handler.rs | 124 ++++++++++-------- .../node_graph/document_node_definitions.rs | 8 +- .../messages/portfolio/document_migration.rs | 2 +- .../interpreted-executor/src/node_registry.rs | 2 - .../libraries/graphic-types/src/graphic.rs | 4 +- .../libraries/rendering/src/renderer.rs | 16 ++- node-graph/nodes/brush/src/brush.rs | 2 +- .../nodes/gcore/src/context_modification.rs | 1 - node-graph/nodes/graphic/src/graphic.rs | 32 ++--- node-graph/nodes/path-bool/src/lib.rs | 4 +- .../vector/src/vector_modification_nodes.rs | 13 +- node-graph/nodes/vector/src/vector_nodes.rs | 12 +- 12 files changed, 120 insertions(+), 100 deletions(-) diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index 26bcdc9daa..2bfb11944b 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -87,6 +87,7 @@ impl DataPanelMessageHandler { current_depth: 0, desired_path: &mut self.element_path, network_interface: &*network_interface, + node_lookup_network_path: Vec::new(), breadcrumbs: Vec::new(), vector_table_tab: self.active_vector_table_tab, }; @@ -154,6 +155,10 @@ struct LayoutData<'a> { current_depth: usize, desired_path: &'a mut Vec, network_interface: &'a NodeNetworkInterface, + /// The `network_path` to use when resolving a `NodeId` cell or leaf page against the network interface. + /// Defaults to root (`&[]`); `Table` rendering temporarily sets it to the path's prefix so nested + /// layers (e.g. inside a Ctrl+M-merged custom subgraph) resolve correctly. + node_lookup_network_path: Vec, breadcrumbs: Vec, vector_table_tab: VectorTableTab, } @@ -171,6 +176,11 @@ macro_rules! generate_layout_downcast { } // TODO: We simply try all these types sequentially. Find a better strategy. fn generate_layout(introspected_data: &Arc, data: &mut LayoutData) -> Option> { + // `Table` is interpreted as a path (e.g. the value produced by `path_of_subgraph`), shown as a + // table where each row's NodeId resolves against the prefix made up of the rows above it. + if let Some(io) = introspected_data.downcast_ref::>>() { + return Some(table_node_id_path_layout_with_breadcrumb(&io.output, data)); + } generate_layout_downcast!(introspected_data, data, [ Table, Table, @@ -180,7 +190,6 @@ fn generate_layout(introspected_data: &Arc, Table, Table, - Table, Table, Table, GradientStops, @@ -760,10 +769,9 @@ impl TableRowLayout for AlphaBlending { } } -/// Resolves the cell/breadcrumb label for a `NodeId` from the root network's metadata, falling back -/// to "Node {id}" if the node isn't present (e.g. an ID that no longer maps to a real node). -fn node_id_display_label(node_id: NodeId, network_interface: &NodeNetworkInterface) -> String { - let network_path: &[NodeId] = &[]; +/// Resolves the cell/breadcrumb label for a `NodeId` against `network_interface` at the given `network_path`, +/// falling back to "Node {id}" if the node isn't present (e.g. an ID that no longer maps to a real node). +fn node_id_display_label(node_id: NodeId, network_interface: &NodeNetworkInterface, network_path: &[NodeId]) -> String { if network_interface.node_metadata(&node_id, network_path).is_some() { network_interface.display_name(&node_id, network_path) } else { @@ -780,13 +788,15 @@ impl TableRowLayout for NodeId { } // Override so the breadcrumb uses the same resolved display name as the cell button, instead of the bare-ID fallback `identifier()` returns. fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec { - data.breadcrumbs.push(node_id_display_label(*self, data.network_interface)); + data.breadcrumbs.push(node_id_display_label(*self, data.network_interface, &data.node_lookup_network_path)); self.element_page(data) } - // Cell label resolves the node's display name via the network interface (looked up at the root network) so the - // button reads as the name shown in the Node Graph / Layers panels. Falls back to "Node {id}" if the lookup misses. + // Cell label resolves the node's display name via the network interface so the button reads as the name shown + // in the Node Graph / Layers panels. The lookup uses `data.node_lookup_network_path` (set by the enclosing + // `Table` if rendering a path) so the resolution succeeds at any nesting depth. Falls back to + // "Node {id}" if the lookup misses. fn cell_widget(&self, target: PathStep, data: &LayoutData) -> WidgetInstance { - let label = node_id_display_label(*self, data.network_interface); + let label = node_id_display_label(*self, data.network_interface, &data.node_lookup_network_path); TextButton::new(label) .on_update(move |_| DataPanelMessage::PushToElementPath { step: target.clone() }.into()) .narrow(true) @@ -795,16 +805,15 @@ impl TableRowLayout for NodeId { // The leaf page shows the node's kind, name, lock/visibility toggles, and a "Make Selected" action button. fn element_page(&self, data: &mut LayoutData) -> Vec { let node_id = *self; - // Layer NodeIds (e.g. via the `editor:layer` attribute) live at the root network; if the lookup misses we just show the placeholder name. - let network_path: &[NodeId] = &[]; - let known = data.network_interface.node_metadata(&node_id, network_path).is_some(); + let network_path = data.node_lookup_network_path.clone(); + let known = data.network_interface.node_metadata(&node_id, &network_path).is_some(); let name = if known { - data.network_interface.display_name(&node_id, network_path) + data.network_interface.display_name(&node_id, &network_path) } else { - "(node not found in root network)".to_string() + "(node not found)".to_string() }; let kind_widget = if known { - let icon = if data.network_interface.is_layer(&node_id, network_path) { "Layer" } else { "Node" }; + let icon = if data.network_interface.is_layer(&node_id, &network_path) { "Layer" } else { "Node" }; IconLabel::new(icon).widget_instance() } else { TextLabel::new("-").widget_instance() @@ -813,8 +822,8 @@ impl TableRowLayout for NodeId { let mut header = vec![kind_widget, Separator::new(SeparatorStyle::Related).widget_instance(), TextLabel::new(name).widget_instance()]; if known { - let is_locked = data.network_interface.is_locked(&node_id, network_path); - let is_visible = data.network_interface.is_visible(&node_id, network_path); + let is_locked = data.network_interface.is_locked(&node_id, &network_path); + let is_visible = data.network_interface.is_visible(&node_id, &network_path); header.push(Separator::new(SeparatorStyle::Unrelated).widget_instance()); header.push( @@ -845,42 +854,6 @@ impl TableRowLayout for NodeId { } } -impl TableRowLayout for Option { - fn type_name() -> &'static str { - "NodeId" - } - fn identifier(&self) -> String { - match self { - Some(node_id) => format!("Node {}", node_id), - None => "-".to_string(), - } - } - // Cells defer to `NodeId`'s named cell button for `Some` (so the label reads as the node's display name), - // or render a plain "-" label for `None`. The leaf page likewise defers to `NodeId` for `Some`. - fn cell_widget(&self, target: PathStep, data: &LayoutData) -> WidgetInstance { - match self { - Some(node_id) => node_id.cell_widget(target, data), - None => TextLabel::new("-").narrow(true).widget_instance(), - } - } - // Defer to `NodeId`'s breadcrumb for `Some` so it stays in sync with the cell label; `None` shows just "-". - fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec { - match self { - Some(node_id) => node_id.layout_with_breadcrumb(data), - None => { - data.breadcrumbs.push("-".to_string()); - self.element_page(data) - } - } - } - fn element_page(&self, data: &mut LayoutData) -> Vec { - match self { - Some(node_id) => node_id.element_page(data), - None => vec![LayoutGroup::row(vec![TextLabel::new("-").widget_instance()])], - } - } -} - /// Invokes another macro with the full list of `TableRowLayout`-implementing types whose values may appear /// as attribute cell values. Both the cell-rendering and drilldown-navigation dispatchers iterate this list, /// so adding a new attribute-displayable type is a single edit here. @@ -901,7 +874,6 @@ macro_rules! known_table_row_types { GradientStops, Color, NodeId, - Option, AlphaBlending, DAffine2, DVec2, @@ -941,10 +913,54 @@ fn dispatch_cell_widget(any: &dyn Any, target: PathStep, data: &LayoutData) -> O None } +/// Renders a `Table` as a path: the standard table view, but each row's `NodeId` cell is resolved +/// against the network path made up of all preceding rows. So for a path `[outer, middle, leaf]`, row 0 +/// resolves at root, row 1 resolves at `[outer]`, and row 2 resolves at `[outer, middle]` — letting deeply +/// nested layers display each step's correct name. Drilling into a row drops into that node's leaf page +/// using the same prefix as `network_path`. +fn table_node_id_path_layout_with_breadcrumb(path: &Table, data: &mut LayoutData) -> Vec { + data.breadcrumbs.push(path.identifier()); + + if let Some(step) = data.desired_path.get(data.current_depth).cloned() { + if let PathStep::Element(index) = step + && let Some(node_id) = path.element(index) + { + let prefix: Vec = path.iter_element_values().take(index).copied().collect(); + let saved = std::mem::replace(&mut data.node_lookup_network_path, prefix); + data.current_depth += 1; + let result = node_id.layout_with_breadcrumb(data); + data.current_depth -= 1; + data.node_lookup_network_path = saved; + return result; + } + warn!("Desired path truncated"); + data.desired_path.truncate(data.current_depth); + } + + let mut rows = (0..path.len()) + .map(|index| { + let node_id = path.element(index).unwrap(); + let prefix: Vec = path.iter_element_values().take(index).copied().collect(); + let saved = std::mem::replace(&mut data.node_lookup_network_path, prefix); + let widget = node_id.cell_widget(PathStep::Element(index), data); + data.node_lookup_network_path = saved; + vec![TextLabel::new(format!("{index}")).narrow(true).widget_instance(), widget] + }) + .collect::>(); + rows.insert(0, column_headings(&["", "element"])); + + vec![LayoutGroup::table(rows, false)] +} + /// Type-dispatched recursion into an attribute value for the data panel breadcrumb navigation. /// Mirrors [`dispatch_cell_widget`] but routes to [`TableRowLayout::layout_with_breadcrumb`]. /// Returns `None` for unrecognized types. fn drilldown_attribute_layout(any: &dyn Any, data: &mut LayoutData) -> Option> { + // `Table` is interpreted as a path (e.g. the `editor:layer` attribute), so each row's NodeId cell + // resolves against the prefix made up of preceding rows. Handled before the generic `Table` blanket impl. + if let Some(path) = any.downcast_ref::>() { + return Some(table_node_id_path_layout_with_breadcrumb(path, data)); + } macro_rules! check { ( $($ty:ty),* $(,)? ) => { $( 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 92354d1224..f692b98163 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 @@ -236,7 +236,7 @@ fn document_node_definitions() -> HashMap HashMap HashMap HashMap HashMap, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Option]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Option]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table]), @@ -173,7 +172,6 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => Table>]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Option]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Option]), - async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Option]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Graphic]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => glam::f32::Vec2]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => glam::f32::Affine2]), diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 9ad7fb150f..d480603541 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -141,7 +141,7 @@ fn flatten_graphic_table(content: Table, extract_variant: fn(Graphic fn flatten_recursive(output: &mut Table, current_graphic_table: Table, extract_variant: fn(Graphic) -> Option>) { for current_graphic_row in current_graphic_table.into_iter() { - let layer: Option = current_graphic_row.attribute_cloned_or_default("editor:layer"); + let layer_path: Table = current_graphic_row.attribute_cloned_or_default("editor:layer"); let current_transform: DAffine2 = current_graphic_row.attribute_cloned_or_default("transform"); let current_alpha_blending: AlphaBlending = current_graphic_row.attribute_cloned_or_default("alpha_blending"); @@ -168,7 +168,7 @@ fn flatten_graphic_table(content: Table, extract_variant: fn(Graphic attributes.insert("transform", current_transform * row_transform); attributes.insert("alpha_blending", compose_alpha_blending(current_alpha_blending, row_alpha_blending)); - attributes.insert("editor:layer", layer); + attributes.insert("editor:layer", layer_path.clone()); output.push(TableRow::from_parts(element, attributes)); } diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 861a477baa..c5c7e7b634 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -412,7 +412,8 @@ impl Render for Graphic { metadata.upstream_footprints.insert(element_id, footprint); // TODO: Find a way to handle more than the first row if !table.is_empty() { - let layer: Option = table.attribute_cloned_or_default("editor:layer", 0); + let layer_path: Table = table.attribute_cloned_or_default("editor:layer", 0); + let layer = layer_path.iter_element_values().next_back().copied(); let transform: DAffine2 = table.attribute_cloned_or_default("transform", 0); metadata.first_element_source_id.insert(element_id, layer); @@ -655,7 +656,8 @@ impl Render for Table { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, _element_id: Option) { for index in 0..self.len() { - let layer: Option = self.attribute_cloned_or_default("editor:layer", index); + let layer_path: Table = self.attribute_cloned_or_default("editor:layer", index); + let layer = layer_path.iter_element_values().next_back().copied(); self.element(index).unwrap().collect_metadata(metadata, footprint, layer); } } @@ -805,7 +807,8 @@ impl Render for Table { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { for index in 0..self.len() { let row_transform: DAffine2 = self.attribute_cloned_or_default("transform", index); - let layer: Option = self.attribute_cloned_or_default("editor:layer", index); + let layer_path: Table = self.attribute_cloned_or_default("editor:layer", index); + let layer = layer_path.iter_element_values().next_back().copied(); let element = self.element(index).unwrap(); let mut footprint = footprint; @@ -860,9 +863,9 @@ impl Render for Table { } fn new_ids_from_hash(&mut self, _reference: Option) { - let (elements, layers) = self.element_and_attribute_slices_mut::>("editor:layer"); + let (elements, layers) = self.element_and_attribute_slices_mut::>("editor:layer"); for (element, layer) in elements.iter_mut().zip(layers.iter()) { - element.new_ids_from_hash(*layer); + element.new_ids_from_hash(layer.iter_element_values().next_back().copied()); } } } @@ -1327,7 +1330,8 @@ impl Render for Table { for index in 0..self.len() { let Some(vector) = self.element(index) else { continue }; let transform: DAffine2 = self.attribute_cloned_or_default("transform", index); - let layer: Option = self.attribute_cloned_or_default("editor:layer", index); + let layer_path: Table = self.attribute_cloned_or_default("editor:layer", index); + let layer = layer_path.iter_element_values().next_back().copied(); if let Some(element_id) = caller_element_id.or(layer) { // When recovering element_id from the row's editor:layer tag (because the caller diff --git a/node-graph/nodes/brush/src/brush.rs b/node-graph/nodes/brush/src/brush.rs index 0e18ba94d6..1776a058a3 100644 --- a/node-graph/nodes/brush/src/brush.rs +++ b/node-graph/nodes/brush/src/brush.rs @@ -314,7 +314,7 @@ async fn brush( let transform: DAffine2 = actual_image.attribute_cloned_or_default("transform"); let alpha_blending: AlphaBlending = actual_image.attribute_cloned_or_default("alpha_blending"); - let layer: Option = actual_image.attribute_cloned_or_default("editor:layer"); + let layer: Table = actual_image.attribute_cloned_or_default("editor:layer"); *image.element_mut(0).unwrap() = actual_image.into_element(); image.set_attribute("transform", 0, transform); diff --git a/node-graph/nodes/gcore/src/context_modification.rs b/node-graph/nodes/gcore/src/context_modification.rs index 8e22154e3f..627525ea5e 100644 --- a/node-graph/nodes/gcore/src/context_modification.rs +++ b/node-graph/nodes/gcore/src/context_modification.rs @@ -26,7 +26,6 @@ async fn context_modification( Context -> DAffine2, Context -> Footprint, Context -> DVec2, - Context -> Option, Context -> Table, Context -> Table, Context -> Table, diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 999df04d5b..d3c92b9bf3 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -209,14 +209,16 @@ where result_table } -/// Returns the NodeId of the user-facing parent layer node that encapsulates this sub-network. -/// Used as the value source for stamping the `editor:layer` attribute on each row of a layer's output, -/// which lets editor tools (e.g. selection, click target routing) trace data back to its owning layer. -#[node_macro::node(category(""))] -pub fn parent_layer(_: impl Ctx, node_path: Table) -> Option { - // Get the penultimate element of the node path, or None if the path is too short - let index = node_path.len().wrapping_sub(2); - node_path.element(index).copied() +/// Returns the path identifying the subgraph (network) that contains this proto node — i.e. the input `node_path` +/// with its own trailing entry dropped. The terminating element of the returned path is the document node whose +/// encapsulated network we live in, so the path doubles as a unique reference to that node at any nesting depth. +/// Used as the value source for stamping the `editor:layer` attribute on each row of a layer's output, which lets +/// editor tools (e.g. selection, click target routing) trace data back to its owning layer regardless of whether +/// the layer is at the root document network or nested inside a custom subgraph. +#[node_macro::node(name("Path of Subgraph"), category(""))] +pub fn path_of_subgraph(_: impl Ctx, node_path: Table) -> Table { + let len = node_path.len(); + node_path.into_iter().take(len.saturating_sub(1)).collect() } /// Writes a per-row attribute column on the input table. The value-producing input is evaluated once per row, @@ -241,13 +243,13 @@ async fn write_attribute f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Option, Context -> Table, Context -> Table, - Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Option, Context -> Table, Context -> Table, - Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Option, Context -> Table, Context -> Table, - Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Option, Context -> Table, Context -> Table, - Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Option, Context -> Table, Context -> Table, - Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Option, Context -> Table, Context -> Table, - Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Option, Context -> Table, Context -> Table, + Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Table, Context -> Table, Context -> Table, + Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Table, Context -> Table, Context -> Table, + Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Table, Context -> Table, Context -> Table, + Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Table, Context -> Table, Context -> Table, + Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Table, Context -> Table, Context -> Table, + Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Table, Context -> Table, Context -> Table, + Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Table, Context -> Table, Context -> Table, )] value: impl Node<'n, Context<'static>, Output = U>, ) -> Table { diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index 4076ed5586..aea0cfdb74 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -197,7 +197,7 @@ fn flatten_vector(graphic_table: &Table) -> Table { (0..image.len()) .map(|i| { let row_transform: DAffine2 = image.attribute_cloned_or_default("transform", i); - let layer: Option = image.attribute_cloned_or_default("editor:layer", i); + let layer: Table = image.attribute_cloned_or_default("editor:layer", i); let alpha_blending: AlphaBlending = image.attribute_cloned_or_default("alpha_blending", i); make_row(parent_transform * row_transform, layer, alpha_blending) }) @@ -223,7 +223,7 @@ fn flatten_vector(graphic_table: &Table) -> Table { (0..image.len()) .map(|i| { let row_transform: DAffine2 = image.attribute_cloned_or_default("transform", i); - let layer: Option = image.attribute_cloned_or_default("editor:layer", i); + let layer: Table = image.attribute_cloned_or_default("editor:layer", i); let alpha_blending: AlphaBlending = image.attribute_cloned_or_default("alpha_blending", i); make_row(parent_transform * row_transform, layer, alpha_blending) }) diff --git a/node-graph/nodes/vector/src/vector_modification_nodes.rs b/node-graph/nodes/vector/src/vector_modification_nodes.rs index f305d5066b..69eceda02a 100644 --- a/node-graph/nodes/vector/src/vector_modification_nodes.rs +++ b/node-graph/nodes/vector/src/vector_modification_nodes.rs @@ -15,13 +15,14 @@ async fn path_modify(_ctx: impl Ctx, mut vector: Table, modification: Bo } modification.apply(vector.element_mut(0).expect("push should give one item")); - // Update the source node id (penultimate element in the path, identifying the user-facing layer node) - let this_node_path = { - let index = node_path.len().wrapping_sub(2); - node_path.element(index).copied() + // Set the path to the encapsulating subgraph (drop our own trailing entry from `node_path`), + // matching the `path_of_subgraph` proto so editor tools can route data back to the parent layer. + let subgraph_path: Table = { + let len = node_path.len(); + node_path.into_iter().take(len.saturating_sub(1)).collect() }; - let existing: Option = vector.attribute_cloned_or_default("editor:layer", 0); - vector.set_attribute("editor:layer", 0, existing.or(this_node_path)); + let existing: Table = vector.attribute_cloned_or_default("editor:layer", 0); + vector.set_attribute("editor:layer", 0, if existing.is_empty() { subgraph_path } else { existing }); if vector.len() > 1 { warn!("The path modify ran on {} vector rows. Only the first can be modified.", vector.len()); diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 465a66105e..e311c03316 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -1296,8 +1296,8 @@ pub async fn flatten_path(_: impl Ctx, #[implem // Concatenate every vector element's subpaths into the single output compound path for index in 0..flattened.len() { let Some(element) = flattened.element(index) else { continue }; - let node_id: Option = flattened.attribute_cloned_or_default("editor:layer", index); - let node_id = node_id.map(|node_id| node_id.0).unwrap_or_default(); + let layer_path: Table = flattened.attribute_cloned_or_default("editor:layer", index); + let node_id = layer_path.iter_element_values().next_back().map(|node_id| node_id.0).unwrap_or_default(); let mut hasher = DefaultHasher::new(); (index, node_id).hash(&mut hasher); @@ -1318,8 +1318,8 @@ pub async fn flatten_path(_: impl Ctx, #[implem // Adopt the last input row's layer so the editor can also bucket clicks under a contributing child layer if !flattened.is_empty() { let primary = flattened.len() - 1; - let layer: Option = flattened.attribute_cloned_or_default("editor:layer", primary); - output_table.set_attribute("editor:layer", 0, layer); + let layer_path: Table = flattened.attribute_cloned_or_default("editor:layer", primary); + output_table.set_attribute("editor:layer", 0, layer_path); } output_table @@ -2529,13 +2529,13 @@ async fn morph( // The result is a synthesis of source and target, so adopt whichever endpoint the result is closer to as // the click-target identity (so the editor can route clicks back to one of the contributing layers) let primary_index = if time < 0.5 { source_index } else { target_index }; - let layer: Option = content.attribute_cloned_or_default("editor:layer", primary_index); + let layer_path: Table = content.attribute_cloned_or_default("editor:layer", primary_index); Table::new_from_row( TableRow::new_from_element(vector) .with_attribute("transform", lerped_transform) .with_attribute("alpha_blending", vector_alpha_blending) - .with_attribute("editor:layer", layer) + .with_attribute("editor:layer", layer_path) .with_attribute("editor:merged_layers", graphic_table_content), ) } From 9d5de95a0448190ff2cab4223aad02fd82911158 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 28 Apr 2026 00:59:55 -0700 Subject: [PATCH 4/7] Polish the data panel NodeId leaf page with an editable name field --- .../data_panel/data_panel_message_handler.rs | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index 2bfb11944b..d68fb4bd2d 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -793,33 +793,55 @@ impl TableRowLayout for NodeId { } // Cell label resolves the node's display name via the network interface so the button reads as the name shown // in the Node Graph / Layers panels. The lookup uses `data.node_lookup_network_path` (set by the enclosing - // `Table` if rendering a path) so the resolution succeeds at any nesting depth. Falls back to - // "Node {id}" if the lookup misses. + // `Table` if rendering a path) so the resolution succeeds at any nesting depth. The button's icon + // signals layer-vs-node kind. Falls back to "Node {id}" with no icon if the lookup misses. fn cell_widget(&self, target: PathStep, data: &LayoutData) -> WidgetInstance { let label = node_id_display_label(*self, data.network_interface, &data.node_lookup_network_path); - TextButton::new(label) + let mut button = TextButton::new(label) .on_update(move |_| DataPanelMessage::PushToElementPath { step: target.clone() }.into()) - .narrow(true) - .widget_instance() + .narrow(true); + if data.network_interface.node_metadata(self, &data.node_lookup_network_path).is_some() { + let icon = if data.network_interface.is_layer(self, &data.node_lookup_network_path) { "Layer" } else { "Node" }; + button = button.icon(icon); + } + button.widget_instance() } - // The leaf page shows the node's kind, name, lock/visibility toggles, and a "Make Selected" action button. + // The leaf page shows the node's kind, name (editable), lock/visibility toggles, and a "Select Layer/Node" action button. fn element_page(&self, data: &mut LayoutData) -> Vec { let node_id = *self; let network_path = data.node_lookup_network_path.clone(); let known = data.network_interface.node_metadata(&node_id, &network_path).is_some(); + let is_layer = known && data.network_interface.is_layer(&node_id, &network_path); let name = if known { data.network_interface.display_name(&node_id, &network_path) } else { "(node not found)".to_string() }; let kind_widget = if known { - let icon = if data.network_interface.is_layer(&node_id, &network_path) { "Layer" } else { "Node" }; - IconLabel::new(icon).widget_instance() + IconLabel::new(if is_layer { "Layer" } else { "Node" }).widget_instance() } else { TextLabel::new("-").widget_instance() }; + let name_widget = if known { + let path_for_rename = network_path.clone(); + TextInput::new(name) + .tooltip_description(if is_layer { "Name of this layer." } else { "Name of this node." }) + .on_update(move |text_input| { + NodeGraphMessage::SetDisplayName { + node_id, + network_path: path_for_rename.clone(), + alias: text_input.value.clone(), + skip_adding_history_step: false, + } + .into() + }) + .max_width(200) + .widget_instance() + } else { + TextLabel::new(name).widget_instance() + }; - let mut header = vec![kind_widget, Separator::new(SeparatorStyle::Related).widget_instance(), TextLabel::new(name).widget_instance()]; + let mut header = vec![kind_widget, Separator::new(SeparatorStyle::Related).widget_instance(), name_widget]; if known { let is_locked = data.network_interface.is_locked(&node_id, &network_path); @@ -844,8 +866,8 @@ impl TableRowLayout for NodeId { header.push(Separator::new(SeparatorStyle::Unrelated).widget_instance()); header.push( - TextButton::new("Make Selected") - .tooltip_description("Click to select the node with this ID in the graph.") + TextButton::new(if is_layer { "Select Layer" } else { "Select Node" }) + .tooltip_description(if is_layer { "Click to select this layer." } else { "Click to select this node." }) .on_update(move |_| NodeGraphMessage::SelectedNodesSet { nodes: vec![node_id] }.into()) .widget_instance(), ); From ac165901b7b1cbddae7b915b0d2acb7f874a9ee3 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 28 Apr 2026 14:52:54 -0700 Subject: [PATCH 5/7] Make lock and visibility toggles work for layers in nested subgraphs --- .../new_document_dialog_message_handler.rs | 2 +- .../data_panel/data_panel_message_handler.rs | 13 +++++--- .../document/node_graph/node_graph_message.rs | 8 +++++ .../node_graph/node_graph_message_handler.rs | 33 ++++++++----------- frontend/wrapper/src/editor_wrapper.rs | 4 +-- 5 files changed, 33 insertions(+), 27 deletions(-) diff --git a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs index 4925eecc55..922750ad0d 100644 --- a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs +++ b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs @@ -38,7 +38,7 @@ impl MessageHandler for NewDocumentDialogMessageHa network_path: Vec::new(), alias: "Background".to_string(), }); - responses.add(NodeGraphMessage::SetLocked { node_id, locked: true }); + responses.add(NodeGraphMessage::SetLocked { node_id, network_path: Vec::new(), locked: true }); } else if self.dimensions.x > 0 && self.dimensions.y > 0 { // Finite canvas: create an artboard with the specified dimensions responses.add(GraphOperationMessage::NewArtboard { diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index d68fb4bd2d..dc955bb381 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -53,9 +53,9 @@ impl MessageHandler> for DataPanel } DataPanelMessage::Refresh => { // Re-render against the current network_interface without disturbing introspected_data or the breadcrumb path. - if self.introspected_data.is_some() { - self.update_layout(responses, context); - } + // Always re-renders, even when introspected_data is None, since the header still shows the inspected node's + // name/lock/visibility state from the network interface and that state can change independently of the data. + self.update_layout(responses, context); } DataPanelMessage::PushToElementPath { step } => { @@ -847,19 +847,22 @@ impl TableRowLayout for NodeId { let is_locked = data.network_interface.is_locked(&node_id, &network_path); let is_visible = data.network_interface.is_visible(&node_id, &network_path); + let path_for_lock = network_path.clone(); + let path_for_visibility = network_path.clone(); + header.push(Separator::new(SeparatorStyle::Unrelated).widget_instance()); header.push( IconButton::new(if is_locked { "PadlockLocked" } else { "PadlockUnlocked" }, 24) .hover_icon(if is_locked { "PadlockUnlocked" } else { "PadlockLocked" }) .tooltip_label(if is_locked { "Unlock" } else { "Lock" }) - .on_update(move |_| NodeGraphMessage::ToggleLocked { node_id }.into()) + .on_update(move |_| NodeGraphMessage::ToggleLocked { node_id, network_path: path_for_lock.clone() }.into()) .widget_instance(), ); header.push( IconButton::new(if is_visible { "EyeVisible" } else { "EyeHidden" }, 24) .hover_icon(if is_visible { "EyeHide" } else { "EyeShow" }) .tooltip_label(if is_visible { "Hide" } else { "Show" }) - .on_update(move |_| NodeGraphMessage::ToggleVisibility { node_id }.into()) + .on_update(move |_| NodeGraphMessage::ToggleVisibility { node_id, network_path: path_for_visibility.clone() }.into()) .widget_instance(), ); } diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs index c03c5a7500..e892eab4ea 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs @@ -203,15 +203,22 @@ pub enum NodeGraphMessage { ToggleSelectedLocked, ToggleLocked { node_id: NodeId, + /// The path to the network containing `node_id`. Empty for nodes at the root document network. + /// Lets the toggle target a node at any nesting depth, independent of the current selection network. + network_path: Vec, }, SetLocked { node_id: NodeId, + network_path: Vec, locked: bool, }, ToggleSelectedIsPinned, ToggleSelectedVisibility, ToggleVisibility { node_id: NodeId, + /// The path to the network containing `node_id`. Empty for nodes at the root document network. + /// Lets the toggle target a node at any nesting depth, independent of the current selection network. + network_path: Vec, }, SetPinned { node_id: NodeId, @@ -219,6 +226,7 @@ pub enum NodeGraphMessage { }, SetVisibility { node_id: NodeId, + network_path: Vec, visible: bool, }, SetLockedOrVisibilitySideEffects { diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index da11918ac2..ea5a78e41c 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -910,13 +910,13 @@ impl<'a> MessageHandler> for NodeG // Toggle visibility of clicked node and return if let Some(clicked_visibility) = network_interface.layer_click_target_from_click(click, network_interface::LayerClickTargetTypes::Visibility, selection_network_path) { - responses.add(NodeGraphMessage::ToggleVisibility { node_id: clicked_visibility }); + responses.add(NodeGraphMessage::ToggleVisibility { node_id: clicked_visibility, network_path: selection_network_path.to_vec() }); return; } // Toggle lock of clicked node and return if let Some(clicked_lock) = network_interface.layer_click_target_from_click(click, network_interface::LayerClickTargetTypes::Lock, selection_network_path) { - responses.add(NodeGraphMessage::ToggleLocked { node_id: clicked_lock }); + responses.add(NodeGraphMessage::ToggleLocked { node_id: clicked_lock, network_path: selection_network_path.to_vec() }); return; } @@ -1875,25 +1875,20 @@ impl<'a> MessageHandler> for NodeG responses.add(DocumentMessage::AddTransaction); for node_id in &node_ids { - responses.add(NodeGraphMessage::SetLocked { node_id: *node_id, locked }); + responses.add(NodeGraphMessage::SetLocked { node_id: *node_id, network_path: selection_network_path.to_vec(), locked }); } responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids }) } - NodeGraphMessage::ToggleLocked { node_id } => { - let Some(node_metadata) = network_interface.document_network_metadata().persistent_metadata.node_metadata.get(&node_id) else { - log::error!("Cannot get node {node_id:?} in NodeGraphMessage::ToggleLocked"); - return; - }; - - let locked = !node_metadata.persistent_metadata.locked; + NodeGraphMessage::ToggleLocked { node_id, network_path } => { + let locked = !network_interface.is_locked(&node_id, &network_path); responses.add(DocumentMessage::AddTransaction); - responses.add(NodeGraphMessage::SetLocked { node_id, locked }); + responses.add(NodeGraphMessage::SetLocked { node_id, network_path, locked }); responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids: vec![node_id] }) } - NodeGraphMessage::SetLocked { node_id, locked } => { - network_interface.set_locked(&node_id, selection_network_path, locked); + NodeGraphMessage::SetLocked { node_id, network_path, locked } => { + network_interface.set_locked(&node_id, &network_path, locked); } NodeGraphMessage::ToggleSelectedIsPinned => { let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else { @@ -1923,22 +1918,22 @@ impl<'a> MessageHandler> for NodeG responses.add(DocumentMessage::AddTransaction); for node_id in &node_ids { - responses.add(NodeGraphMessage::SetVisibility { node_id: *node_id, visible }); + responses.add(NodeGraphMessage::SetVisibility { node_id: *node_id, network_path: selection_network_path.to_vec(), visible }); } responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids }); } - NodeGraphMessage::ToggleVisibility { node_id } => { - let visible = !network_interface.is_visible(&node_id, selection_network_path); + NodeGraphMessage::ToggleVisibility { node_id, network_path } => { + let visible = !network_interface.is_visible(&node_id, &network_path); responses.add(DocumentMessage::AddTransaction); - responses.add(NodeGraphMessage::SetVisibility { node_id, visible }); + responses.add(NodeGraphMessage::SetVisibility { node_id, network_path, visible }); responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids: vec![node_id] }); } NodeGraphMessage::SetPinned { node_id, pinned } => { network_interface.set_pinned(&node_id, selection_network_path, pinned); } - NodeGraphMessage::SetVisibility { node_id, visible } => { - network_interface.set_visibility(&node_id, selection_network_path, visible); + NodeGraphMessage::SetVisibility { node_id, network_path, visible } => { + network_interface.set_visibility(&node_id, &network_path, visible); } NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids } => { if node_ids.iter().any(|node_id| network_interface.connected_to_output(node_id, selection_network_path)) { diff --git a/frontend/wrapper/src/editor_wrapper.rs b/frontend/wrapper/src/editor_wrapper.rs index b953256c3e..72daaf91b4 100644 --- a/frontend/wrapper/src/editor_wrapper.rs +++ b/frontend/wrapper/src/editor_wrapper.rs @@ -913,7 +913,7 @@ impl EditorWrapper { #[wasm_bindgen(js_name = toggleNodeVisibilityLayerPanel)] pub fn toggle_node_visibility_layer(&self, id: u64) { let node_id = NodeId(id); - let message = NodeGraphMessage::ToggleVisibility { node_id }; + let message = NodeGraphMessage::ToggleVisibility { node_id, network_path: Vec::new() }; self.dispatch(message); } @@ -932,7 +932,7 @@ impl EditorWrapper { /// Toggle lock state of a layer from the layer list #[wasm_bindgen(js_name = toggleLayerLock)] pub fn toggle_layer_lock(&self, node_id: u64) { - let message = NodeGraphMessage::ToggleLocked { node_id: NodeId(node_id) }; + let message = NodeGraphMessage::ToggleLocked { node_id: NodeId(node_id), network_path: Vec::new() }; self.dispatch(message); } From 38ff64128974d3f3921a1b951169aae52a8e91f1 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 28 Apr 2026 14:54:42 -0700 Subject: [PATCH 6/7] Fix formatting --- .../new_document_dialog_message_handler.rs | 6 ++++- .../data_panel/data_panel_message_handler.rs | 16 ++++++++++++-- .../node_graph/node_graph_message_handler.rs | 22 +++++++++++++++---- frontend/wrapper/src/editor_wrapper.rs | 5 ++++- 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs index 922750ad0d..e508cbd5bc 100644 --- a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs +++ b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs @@ -38,7 +38,11 @@ impl MessageHandler for NewDocumentDialogMessageHa network_path: Vec::new(), alias: "Background".to_string(), }); - responses.add(NodeGraphMessage::SetLocked { node_id, network_path: Vec::new(), locked: true }); + responses.add(NodeGraphMessage::SetLocked { + node_id, + network_path: Vec::new(), + locked: true, + }); } else if self.dimensions.x > 0 && self.dimensions.y > 0 { // Finite canvas: create an artboard with the specified dimensions responses.add(GraphOperationMessage::NewArtboard { diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index dc955bb381..d3d95cbc66 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -855,14 +855,26 @@ impl TableRowLayout for NodeId { IconButton::new(if is_locked { "PadlockLocked" } else { "PadlockUnlocked" }, 24) .hover_icon(if is_locked { "PadlockUnlocked" } else { "PadlockLocked" }) .tooltip_label(if is_locked { "Unlock" } else { "Lock" }) - .on_update(move |_| NodeGraphMessage::ToggleLocked { node_id, network_path: path_for_lock.clone() }.into()) + .on_update(move |_| { + NodeGraphMessage::ToggleLocked { + node_id, + network_path: path_for_lock.clone(), + } + .into() + }) .widget_instance(), ); header.push( IconButton::new(if is_visible { "EyeVisible" } else { "EyeHidden" }, 24) .hover_icon(if is_visible { "EyeHide" } else { "EyeShow" }) .tooltip_label(if is_visible { "Hide" } else { "Show" }) - .on_update(move |_| NodeGraphMessage::ToggleVisibility { node_id, network_path: path_for_visibility.clone() }.into()) + .on_update(move |_| { + NodeGraphMessage::ToggleVisibility { + node_id, + network_path: path_for_visibility.clone(), + } + .into() + }) .widget_instance(), ); } diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index ea5a78e41c..bf9e96a108 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -910,13 +910,19 @@ impl<'a> MessageHandler> for NodeG // Toggle visibility of clicked node and return if let Some(clicked_visibility) = network_interface.layer_click_target_from_click(click, network_interface::LayerClickTargetTypes::Visibility, selection_network_path) { - responses.add(NodeGraphMessage::ToggleVisibility { node_id: clicked_visibility, network_path: selection_network_path.to_vec() }); + responses.add(NodeGraphMessage::ToggleVisibility { + node_id: clicked_visibility, + network_path: selection_network_path.to_vec(), + }); return; } // Toggle lock of clicked node and return if let Some(clicked_lock) = network_interface.layer_click_target_from_click(click, network_interface::LayerClickTargetTypes::Lock, selection_network_path) { - responses.add(NodeGraphMessage::ToggleLocked { node_id: clicked_lock, network_path: selection_network_path.to_vec() }); + responses.add(NodeGraphMessage::ToggleLocked { + node_id: clicked_lock, + network_path: selection_network_path.to_vec(), + }); return; } @@ -1875,7 +1881,11 @@ impl<'a> MessageHandler> for NodeG responses.add(DocumentMessage::AddTransaction); for node_id in &node_ids { - responses.add(NodeGraphMessage::SetLocked { node_id: *node_id, network_path: selection_network_path.to_vec(), locked }); + responses.add(NodeGraphMessage::SetLocked { + node_id: *node_id, + network_path: selection_network_path.to_vec(), + locked, + }); } responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids }) @@ -1918,7 +1928,11 @@ impl<'a> MessageHandler> for NodeG responses.add(DocumentMessage::AddTransaction); for node_id in &node_ids { - responses.add(NodeGraphMessage::SetVisibility { node_id: *node_id, network_path: selection_network_path.to_vec(), visible }); + responses.add(NodeGraphMessage::SetVisibility { + node_id: *node_id, + network_path: selection_network_path.to_vec(), + visible, + }); } responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids }); } diff --git a/frontend/wrapper/src/editor_wrapper.rs b/frontend/wrapper/src/editor_wrapper.rs index 72daaf91b4..5e8b04dacc 100644 --- a/frontend/wrapper/src/editor_wrapper.rs +++ b/frontend/wrapper/src/editor_wrapper.rs @@ -932,7 +932,10 @@ impl EditorWrapper { /// Toggle lock state of a layer from the layer list #[wasm_bindgen(js_name = toggleLayerLock)] pub fn toggle_layer_lock(&self, node_id: u64) { - let message = NodeGraphMessage::ToggleLocked { node_id: NodeId(node_id), network_path: Vec::new() }; + let message = NodeGraphMessage::ToggleLocked { + node_id: NodeId(node_id), + network_path: Vec::new(), + }; self.dispatch(message); } From 7ae6b5e9ee5ca787305ee21bc710f9eed713d9d6 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 28 Apr 2026 15:31:09 -0700 Subject: [PATCH 7/7] Fix connected_to_output running in the wrong network for nested-layer toggles --- .../document/node_graph/node_graph_message.rs | 1 + .../node_graph/node_graph_message_handler.rs | 41 +++++++++++++++---- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs index e892eab4ea..f5367e8861 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs @@ -231,6 +231,7 @@ pub enum NodeGraphMessage { }, SetLockedOrVisibilitySideEffects { node_ids: Vec, + network_path: Vec, }, UpdateEdges, UpdateBoxSelection, diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index bf9e96a108..0f8a51298b 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -1888,14 +1888,24 @@ impl<'a> MessageHandler> for NodeG }); } - responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids }) + responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { + node_ids, + network_path: selection_network_path.to_vec(), + }) } NodeGraphMessage::ToggleLocked { node_id, network_path } => { let locked = !network_interface.is_locked(&node_id, &network_path); responses.add(DocumentMessage::AddTransaction); - responses.add(NodeGraphMessage::SetLocked { node_id, network_path, locked }); - responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids: vec![node_id] }) + responses.add(NodeGraphMessage::SetLocked { + node_id, + network_path: network_path.clone(), + locked, + }); + responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { + node_ids: vec![node_id], + network_path, + }); } NodeGraphMessage::SetLocked { node_id, network_path, locked } => { network_interface.set_locked(&node_id, &network_path, locked); @@ -1914,7 +1924,10 @@ impl<'a> MessageHandler> for NodeG for node_id in &node_ids { responses.add(NodeGraphMessage::SetPinned { node_id: *node_id, pinned }); } - responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids }); + responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { + node_ids, + network_path: selection_network_path.to_vec(), + }); } NodeGraphMessage::ToggleSelectedVisibility => { let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else { @@ -1934,14 +1947,24 @@ impl<'a> MessageHandler> for NodeG visible, }); } - responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids }); + responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { + node_ids, + network_path: selection_network_path.to_vec(), + }); } NodeGraphMessage::ToggleVisibility { node_id, network_path } => { let visible = !network_interface.is_visible(&node_id, &network_path); responses.add(DocumentMessage::AddTransaction); - responses.add(NodeGraphMessage::SetVisibility { node_id, network_path, visible }); - responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids: vec![node_id] }); + responses.add(NodeGraphMessage::SetVisibility { + node_id, + network_path: network_path.clone(), + visible, + }); + responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { + node_ids: vec![node_id], + network_path, + }); } NodeGraphMessage::SetPinned { node_id, pinned } => { network_interface.set_pinned(&node_id, selection_network_path, pinned); @@ -1949,8 +1972,8 @@ impl<'a> MessageHandler> for NodeG NodeGraphMessage::SetVisibility { node_id, network_path, visible } => { network_interface.set_visibility(&node_id, &network_path, visible); } - NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids } => { - if node_ids.iter().any(|node_id| network_interface.connected_to_output(node_id, selection_network_path)) { + NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids, network_path } => { + if node_ids.iter().any(|node_id| network_interface.connected_to_output(node_id, &network_path)) { responses.add(NodeGraphMessage::RunDocumentGraph); } responses.add(NodeGraphMessage::UpdateActionButtons);