Make the drawing tools' Weight control sync with the selected layer (#4120)

This commit is contained in:
Keavon Chambers 2026-05-07 02:12:38 -07:00 committed by GitHub
parent 9512f7df41
commit 1596469e92
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 117 additions and 11 deletions

View File

@ -355,7 +355,20 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
responses.add(NodeGraphMessage::DeleteSelectedNodes { delete_children: true }); responses.add(NodeGraphMessage::DeleteSelectedNodes { delete_children: true });
} }
NodeGraphMessage::DeleteNodes { node_ids, delete_children } => { 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); 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. // 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. // If `reconnect` is false, then only the selected nodes are deleted and not reconnected.
@ -1728,9 +1741,9 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
} }
NodeGraphMessage::SetInputValue { node_id, input_index, value } => { NodeGraphMessage::SetInputValue { node_id, input_index, value } => {
let is_fill = matches!(value, TaggedValue::Fill(_)); let is_fill = matches!(value, TaggedValue::Fill(_));
let is_text_node = network_interface let reference = network_interface.reference(&node_id, selection_network_path);
.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));
.is_some_and(|reference| reference == 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); let input = NodeInput::value(value, false);
responses.add(NodeGraphMessage::SetInput { responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, input_index), input_connector: InputConnector::node(node_id, input_index),
@ -1743,6 +1756,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
if is_text_node { if is_text_node {
responses.add(TextToolMessage::SelectionChanged); 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) { if network_interface.connected_to_output(&node_id, selection_network_path) {
responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::RunDocumentGraph);
} }

View File

@ -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<NodeId> {
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<f64> {
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<Message>) {
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. /// 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 { 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() NodeGraphLayer::new(layer, network_interface).find_node_inputs(identifier).is_some()

View File

@ -44,6 +44,7 @@ pub enum FreehandToolMessage {
// Standard messages // Standard messages
Overlays { context: OverlayContext }, Overlays { context: OverlayContext },
Abort, Abort,
SelectionChanged,
WorkingColorChanged, WorkingColorChanged,
// Tool-specific messages // Tool-specific messages
@ -161,6 +162,16 @@ impl LayoutHolder for FreehandTool {
#[message_handler_data] #[message_handler_data]
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for FreehandTool { impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for FreehandTool {
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, 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 { let ToolMessage::Freehand(FreehandToolMessage::UpdateOptions { options }) = message else {
self.fsm_state.process_event(message, &mut self.data, context, &self.options, responses, true); self.fsm_state.process_event(message, &mut self.data, context, &self.options, responses, true);
return; return;
@ -171,7 +182,10 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Free
self.options.fill.color_type = ToolColorType::Custom; self.options.fill.color_type = ToolColorType::Custom;
} }
FreehandOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type, 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) => { FreehandOptionsUpdate::StrokeColor(color) => {
self.options.stroke.custom_color = color; self.options.stroke.custom_color = color;
self.options.stroke.color_type = ToolColorType::Custom; self.options.stroke.color_type = ToolColorType::Custom;
@ -208,6 +222,7 @@ impl ToolTransition for FreehandTool {
EventToMessageMap { EventToMessageMap {
overlay_provider: Some(|context: OverlayContext| FreehandToolMessage::Overlays { context }.into()), overlay_provider: Some(|context: OverlayContext| FreehandToolMessage::Overlays { context }.into()),
tool_abort: Some(FreehandToolMessage::Abort.into()), tool_abort: Some(FreehandToolMessage::Abort.into()),
selection_changed: Some(FreehandToolMessage::SelectionChanged.into()),
working_color_changed: Some(FreehandToolMessage::WorkingColorChanged.into()), working_color_changed: Some(FreehandToolMessage::WorkingColorChanged.into()),
..Default::default() ..Default::default()
} }

View File

@ -249,6 +249,14 @@ impl LayoutHolder for PenTool {
#[message_handler_data] #[message_handler_data]
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for PenTool { impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for PenTool {
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, 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 { let ToolMessage::Pen(PenToolMessage::UpdateOptions { options }) = message else {
self.fsm_state.process_event(message, &mut self.tool_data, context, &self.options, responses, true); self.fsm_state.process_event(message, &mut self.tool_data, context, &self.options, responses, true);
return; return;
@ -259,7 +267,10 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for PenT
self.options.pen_overlay_mode = overlay_mode_type; self.options.pen_overlay_mode = overlay_mode_type;
responses.add(OverlaysMessage::Draw); 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) => { PenOptionsUpdate::FillColor(color) => {
self.options.fill.custom_color = color; self.options.fill.custom_color = color;
self.options.fill.color_type = ToolColorType::Custom; self.options.fill.color_type = ToolColorType::Custom;

View File

@ -95,6 +95,7 @@ pub enum ShapeToolMessage {
// Standard messages // Standard messages
Overlays { context: OverlayContext }, Overlays { context: OverlayContext },
Abort, Abort,
SelectionChanged,
WorkingColorChanged, WorkingColorChanged,
// Tool-specific messages // Tool-specific messages
@ -415,6 +416,16 @@ impl LayoutHolder for ShapeTool {
#[message_handler_data] #[message_handler_data]
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for ShapeTool { impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for ShapeTool {
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, 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 { let ToolMessage::Shape(ShapeToolMessage::UpdateOptions { options }) = message else {
self.fsm_state.process_event(message, &mut self.tool_data, context, &self.options, responses, true); self.fsm_state.process_event(message, &mut self.tool_data, context, &self.options, responses, true);
return; return;
@ -429,6 +440,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Shap
} }
ShapeOptionsUpdate::LineWeight(line_weight) => { ShapeOptionsUpdate::LineWeight(line_weight) => {
self.options.line_weight = 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) => { ShapeOptionsUpdate::StrokeColor(color) => {
self.options.stroke.custom_color = color; self.options.stroke.custom_color = color;
@ -527,6 +539,7 @@ impl ToolTransition for ShapeTool {
EventToMessageMap { EventToMessageMap {
overlay_provider: Some(|context| ShapeToolMessage::Overlays { context }.into()), overlay_provider: Some(|context| ShapeToolMessage::Overlays { context }.into()),
tool_abort: Some(ShapeToolMessage::Abort.into()), tool_abort: Some(ShapeToolMessage::Abort.into()),
selection_changed: Some(ShapeToolMessage::SelectionChanged.into()),
working_color_changed: Some(ShapeToolMessage::WorkingColorChanged.into()), working_color_changed: Some(ShapeToolMessage::WorkingColorChanged.into()),
..Default::default() ..Default::default()
} }

View File

@ -46,6 +46,7 @@ pub enum SplineToolMessage {
Overlays { context: OverlayContext }, Overlays { context: OverlayContext },
CanvasTransformed, CanvasTransformed,
Abort, Abort,
SelectionChanged,
WorkingColorChanged, WorkingColorChanged,
// Tool-specific messages // Tool-specific messages
@ -168,12 +169,25 @@ impl LayoutHolder for SplineTool {
#[message_handler_data] #[message_handler_data]
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for SplineTool { impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for SplineTool {
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, 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 { let ToolMessage::Spline(SplineToolMessage::UpdateOptions { options }) = message else {
self.fsm_state.process_event(message, &mut self.tool_data, context, &self.options, responses, true); self.fsm_state.process_event(message, &mut self.tool_data, context, &self.options, responses, true);
return; return;
}; };
match options { 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) => { SplineOptionsUpdate::FillColor(color) => {
self.options.fill.custom_color = color; self.options.fill.custom_color = color;
self.options.fill.color_type = ToolColorType::Custom; self.options.fill.color_type = ToolColorType::Custom;
@ -224,8 +238,8 @@ impl ToolTransition for SplineTool {
overlay_provider: Some(|context: OverlayContext| SplineToolMessage::Overlays { context }.into()), overlay_provider: Some(|context: OverlayContext| SplineToolMessage::Overlays { context }.into()),
canvas_transformed: Some(SplineToolMessage::CanvasTransformed.into()), canvas_transformed: Some(SplineToolMessage::CanvasTransformed.into()),
tool_abort: Some(SplineToolMessage::Abort.into()), tool_abort: Some(SplineToolMessage::Abort.into()),
selection_changed: Some(SplineToolMessage::SelectionChanged.into()),
working_color_changed: Some(SplineToolMessage::WorkingColorChanged.into()), working_color_changed: Some(SplineToolMessage::WorkingColorChanged.into()),
..Default::default()
} }
} }
} }

View File

@ -99,9 +99,9 @@ impl ToolMetadata for TextTool {
} }
fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog, document: &DocumentMessageHandler) -> Vec<WidgetInstance> { fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog, document: &DocumentMessageHandler) -> Vec<WidgetInstance> {
// 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). // 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 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; let font_input_index = graphene_std::text::text::FontInput::INDEX;
@ -324,8 +324,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
}; };
match options { match options {
TextOptionsUpdate::Font { font } => { TextOptionsUpdate::Font { font } => {
// The toolbar font/style menus go through `SetInputValue` directly when a text layer is selected, so this // 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 (toolbar font is just the default for the next-created text). // 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(); self.options.font = font.clone();
if let Some(editing_text) = self.tool_data.editing_text.as_mut() { if let Some(editing_text) = self.tool_data.editing_text.as_mut() {
editing_text.font = font; editing_text.font = font;