From 1596469e92550daa083c0440fe06dc73120c2ad2 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 7 May 2026 02:12:38 -0700 Subject: [PATCH] Make the drawing tools' Weight control sync with the selected layer (#4120) --- .../node_graph/node_graph_message_handler.rs | 26 +++++++++++++-- .../graph_modification_utils.rs | 33 +++++++++++++++++++ .../tool/tool_messages/freehand_tool.rs | 17 +++++++++- .../messages/tool/tool_messages/pen_tool.rs | 13 +++++++- .../messages/tool/tool_messages/shape_tool.rs | 13 ++++++++ .../tool/tool_messages/spline_tool.rs | 18 ++++++++-- .../messages/tool/tool_messages/text_tool.rs | 8 ++--- 7 files changed, 117 insertions(+), 11 deletions(-) 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 91df5586..a4b94f2e 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 @@ -355,7 +355,20 @@ impl<'a> MessageHandler> for NodeG responses.add(NodeGraphMessage::DeleteSelectedNodes { delete_children: true }); } NodeGraphMessage::DeleteNodes { node_ids, delete_children } => { + // Detect stroke proto nodes among the doomed nodes before they're gone, so the stroke-using tools' + // Weight widgets can re-read the layer (they'll now read 0 px since the stroke node is missing). + let any_stroke_deleted = node_ids.iter().any(|node_id| { + network_interface + .reference(node_id, selection_network_path) + .is_some_and(|reference| reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER)) + }); network_interface.delete_nodes(node_ids, delete_children, selection_network_path); + if any_stroke_deleted { + responses.add(PenToolMessage::SelectionChanged); + responses.add(FreehandToolMessage::SelectionChanged); + responses.add(SplineToolMessage::SelectionChanged); + responses.add(ShapeToolMessage::SelectionChanged); + } } // Deletes selected_nodes. If `reconnect` is true, then all children nodes (secondary input) of the selected nodes are deleted and the siblings (primary input/output) are reconnected. // If `reconnect` is false, then only the selected nodes are deleted and not reconnected. @@ -1728,9 +1741,9 @@ impl<'a> MessageHandler> for NodeG } NodeGraphMessage::SetInputValue { node_id, input_index, value } => { let is_fill = matches!(value, TaggedValue::Fill(_)); - let is_text_node = network_interface - .reference(&node_id, selection_network_path) - .is_some_and(|reference| reference == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER)); + let reference = network_interface.reference(&node_id, selection_network_path); + let is_text_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER)); + let is_stroke_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER)); let input = NodeInput::value(value, false); responses.add(NodeGraphMessage::SetInput { input_connector: InputConnector::node(node_id, input_index), @@ -1743,6 +1756,13 @@ impl<'a> MessageHandler> for NodeG if is_text_node { responses.add(TextToolMessage::SelectionChanged); } + if is_stroke_node { + // The dispatcher delivers each only to its tool when active, so this just covers all four stroke-using tools. + responses.add(PenToolMessage::SelectionChanged); + responses.add(FreehandToolMessage::SelectionChanged); + responses.add(SplineToolMessage::SelectionChanged); + responses.add(ShapeToolMessage::SelectionChanged); + } if network_interface.connected_to_output(&node_id, selection_network_path) { responses.add(NodeGraphMessage::RunDocumentGraph); } 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 6e26cd78..4f7502ec 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -493,6 +493,39 @@ pub fn get_stroke_width(layer: LayerNodeIdentifier, network_interface: &NodeNetw } } +/// Returns the node ID of a layer's upstream Stroke proto node, if one exists. +pub fn get_stroke_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { + NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER)) +} + +/// Stroke weight of the first selected non-artboard layer, used by tool control bars to mirror the selection's weight. +/// Returns `Some(0.)` if the layer has no Stroke node so the widget reads "0 px", and `None` only when no layer is selected. +pub fn first_selected_stroke_weight(document: &DocumentMessageHandler) -> Option { + document + .network_interface + .selected_nodes() + .selected_layers_except_artboards(&document.network_interface) + .next() + .map(|layer| get_stroke_width(layer, &document.network_interface).unwrap_or(0.)) +} + +/// Writes the weight back to every selected non-artboard layer's stroke. Layers with an existing stroke just have their +/// `WeightInput` updated; layers without one get a fresh stroke node added (defaulting to a black stroke with the new +/// weight) only when the new weight is nonzero, so changing back to 0 doesn't keep adding empty strokes. +pub fn set_stroke_weight_for_selected_layers(weight: f64, document: &DocumentMessageHandler, responses: &mut VecDeque) { + let layers: Vec<_> = document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface).collect(); + for layer in layers { + if let Some(node_id) = get_stroke_id(layer, &document.network_interface) { + let input_index = graphene_std::vector::stroke::WeightInput::INDEX; + let value = TaggedValue::F64(weight); + responses.add(NodeGraphMessage::SetInputValue { node_id, input_index, value }); + } else if weight > 0. { + let stroke = graphene_std::vector::style::Stroke::default().with_weight(weight); + responses.add(GraphOperationMessage::StrokeSet { layer, stroke }); + } + } +} + /// Checks if a specified layer uses an upstream node matching the given name. pub fn is_layer_fed_by_node_of_name(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface, identifier: &DefinitionIdentifier) -> bool { NodeGraphLayer::new(layer, network_interface).find_node_inputs(identifier).is_some() diff --git a/editor/src/messages/tool/tool_messages/freehand_tool.rs b/editor/src/messages/tool/tool_messages/freehand_tool.rs index d2a7a198..94765fd5 100644 --- a/editor/src/messages/tool/tool_messages/freehand_tool.rs +++ b/editor/src/messages/tool/tool_messages/freehand_tool.rs @@ -44,6 +44,7 @@ pub enum FreehandToolMessage { // Standard messages Overlays { context: OverlayContext }, Abort, + SelectionChanged, WorkingColorChanged, // Tool-specific messages @@ -161,6 +162,16 @@ impl LayoutHolder for FreehandTool { #[message_handler_data] impl<'a> MessageHandler> for FreehandTool { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, context: &mut ToolActionMessageContext<'a>) { + if matches!(&message, ToolMessage::Freehand(FreehandToolMessage::SelectionChanged)) { + if let Some(weight) = graph_modification_utils::first_selected_stroke_weight(context.document) + && self.options.line_weight != weight + { + self.options.line_weight = weight; + self.send_layout(responses, LayoutTarget::ToolOptions); + } + return; + } + let ToolMessage::Freehand(FreehandToolMessage::UpdateOptions { options }) = message else { self.fsm_state.process_event(message, &mut self.data, context, &self.options, responses, true); return; @@ -171,7 +182,10 @@ impl<'a> MessageHandler> for Free self.options.fill.color_type = ToolColorType::Custom; } FreehandOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type, - FreehandOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight, + FreehandOptionsUpdate::LineWeight(line_weight) => { + self.options.line_weight = line_weight; + graph_modification_utils::set_stroke_weight_for_selected_layers(line_weight, context.document, responses); + } FreehandOptionsUpdate::StrokeColor(color) => { self.options.stroke.custom_color = color; self.options.stroke.color_type = ToolColorType::Custom; @@ -208,6 +222,7 @@ impl ToolTransition for FreehandTool { EventToMessageMap { overlay_provider: Some(|context: OverlayContext| FreehandToolMessage::Overlays { context }.into()), tool_abort: Some(FreehandToolMessage::Abort.into()), + selection_changed: Some(FreehandToolMessage::SelectionChanged.into()), working_color_changed: Some(FreehandToolMessage::WorkingColorChanged.into()), ..Default::default() } diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 00533ffb..5ef9381c 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -249,6 +249,14 @@ impl LayoutHolder for PenTool { #[message_handler_data] impl<'a> MessageHandler> for PenTool { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, context: &mut ToolActionMessageContext<'a>) { + if matches!(&message, ToolMessage::Pen(PenToolMessage::SelectionChanged)) + && let Some(weight) = graph_modification_utils::first_selected_stroke_weight(context.document) + && self.options.line_weight != weight + { + self.options.line_weight = weight; + self.send_layout(responses, LayoutTarget::ToolOptions); + } + let ToolMessage::Pen(PenToolMessage::UpdateOptions { options }) = message else { self.fsm_state.process_event(message, &mut self.tool_data, context, &self.options, responses, true); return; @@ -259,7 +267,10 @@ impl<'a> MessageHandler> for PenT self.options.pen_overlay_mode = overlay_mode_type; responses.add(OverlaysMessage::Draw); } - PenOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight, + PenOptionsUpdate::LineWeight(line_weight) => { + self.options.line_weight = line_weight; + graph_modification_utils::set_stroke_weight_for_selected_layers(line_weight, context.document, responses); + } PenOptionsUpdate::FillColor(color) => { self.options.fill.custom_color = color; self.options.fill.color_type = ToolColorType::Custom; diff --git a/editor/src/messages/tool/tool_messages/shape_tool.rs b/editor/src/messages/tool/tool_messages/shape_tool.rs index fcad13c5..df28b767 100644 --- a/editor/src/messages/tool/tool_messages/shape_tool.rs +++ b/editor/src/messages/tool/tool_messages/shape_tool.rs @@ -95,6 +95,7 @@ pub enum ShapeToolMessage { // Standard messages Overlays { context: OverlayContext }, Abort, + SelectionChanged, WorkingColorChanged, // Tool-specific messages @@ -415,6 +416,16 @@ impl LayoutHolder for ShapeTool { #[message_handler_data] impl<'a> MessageHandler> for ShapeTool { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, context: &mut ToolActionMessageContext<'a>) { + if matches!(&message, ToolMessage::Shape(ShapeToolMessage::SelectionChanged)) { + if let Some(weight) = graph_modification_utils::first_selected_stroke_weight(context.document) + && self.options.line_weight != weight + { + self.options.line_weight = weight; + self.send_layout(responses, LayoutTarget::ToolOptions); + } + return; + } + let ToolMessage::Shape(ShapeToolMessage::UpdateOptions { options }) = message else { self.fsm_state.process_event(message, &mut self.tool_data, context, &self.options, responses, true); return; @@ -429,6 +440,7 @@ impl<'a> MessageHandler> for Shap } ShapeOptionsUpdate::LineWeight(line_weight) => { self.options.line_weight = line_weight; + graph_modification_utils::set_stroke_weight_for_selected_layers(line_weight, context.document, responses); } ShapeOptionsUpdate::StrokeColor(color) => { self.options.stroke.custom_color = color; @@ -527,6 +539,7 @@ impl ToolTransition for ShapeTool { EventToMessageMap { overlay_provider: Some(|context| ShapeToolMessage::Overlays { context }.into()), tool_abort: Some(ShapeToolMessage::Abort.into()), + selection_changed: Some(ShapeToolMessage::SelectionChanged.into()), working_color_changed: Some(ShapeToolMessage::WorkingColorChanged.into()), ..Default::default() } diff --git a/editor/src/messages/tool/tool_messages/spline_tool.rs b/editor/src/messages/tool/tool_messages/spline_tool.rs index 62e9b8e6..1409a94c 100644 --- a/editor/src/messages/tool/tool_messages/spline_tool.rs +++ b/editor/src/messages/tool/tool_messages/spline_tool.rs @@ -46,6 +46,7 @@ pub enum SplineToolMessage { Overlays { context: OverlayContext }, CanvasTransformed, Abort, + SelectionChanged, WorkingColorChanged, // Tool-specific messages @@ -168,12 +169,25 @@ impl LayoutHolder for SplineTool { #[message_handler_data] impl<'a> MessageHandler> for SplineTool { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, context: &mut ToolActionMessageContext<'a>) { + if matches!(&message, ToolMessage::Spline(SplineToolMessage::SelectionChanged)) { + if let Some(weight) = graph_modification_utils::first_selected_stroke_weight(context.document) + && self.options.line_weight != weight + { + self.options.line_weight = weight; + self.send_layout(responses, LayoutTarget::ToolOptions); + } + return; + } + let ToolMessage::Spline(SplineToolMessage::UpdateOptions { options }) = message else { self.fsm_state.process_event(message, &mut self.tool_data, context, &self.options, responses, true); return; }; match options { - SplineOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight, + SplineOptionsUpdate::LineWeight(line_weight) => { + self.options.line_weight = line_weight; + graph_modification_utils::set_stroke_weight_for_selected_layers(line_weight, context.document, responses); + } SplineOptionsUpdate::FillColor(color) => { self.options.fill.custom_color = color; self.options.fill.color_type = ToolColorType::Custom; @@ -224,8 +238,8 @@ impl ToolTransition for SplineTool { overlay_provider: Some(|context: OverlayContext| SplineToolMessage::Overlays { context }.into()), canvas_transformed: Some(SplineToolMessage::CanvasTransformed.into()), tool_abort: Some(SplineToolMessage::Abort.into()), + selection_changed: Some(SplineToolMessage::SelectionChanged.into()), working_color_changed: Some(SplineToolMessage::WorkingColorChanged.into()), - ..Default::default() } } } diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index dbc10ee7..f8f28fad 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -99,9 +99,9 @@ impl ToolMetadata for TextTool { } fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog, document: &DocumentMessageHandler) -> Vec { - // If a single text layer is selected, the toolbar's font/style menus drive that layer's text node directly, going through the + // If a single text layer is selected, the control bar's font/style menus drive that layer's text node directly, going through the // same code path as the Properties panel (LoadFontData + SetInputValue, with closest_style and font_style_to_restore bookkeeping). - // Otherwise the menus only update the toolbar option for the next created text. + // Otherwise the menus only update the control bar option for the next created text. let text_node_id = can_edit_selected(document).and_then(|layer| graph_modification_utils::get_text_id(layer, &document.network_interface)); let font_input_index = graphene_std::text::text::FontInput::INDEX; @@ -324,8 +324,8 @@ impl<'a> MessageHandler> for Text }; match options { TextOptionsUpdate::Font { font } => { - // The toolbar font/style menus go through `SetInputValue` directly when a text layer is selected, so this - // arm only fires when no layer is selected (toolbar font is just the default for the next-created text). + // The control bar font/style menus go through `SetInputValue` directly when a text layer is selected, so this + // arm only fires when no layer is selected (control bar font is just the default for the next-created text). self.options.font = font.clone(); if let Some(editing_text) = self.tool_data.editing_text.as_mut() { editing_text.font = font;