Skip to content

Commit 1596469

Browse files
authored
Make the drawing tools' Weight control sync with the selected layer (#4120)
1 parent 9512f7d commit 1596469

7 files changed

Lines changed: 117 additions & 11 deletions

File tree

editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,20 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
355355
responses.add(NodeGraphMessage::DeleteSelectedNodes { delete_children: true });
356356
}
357357
NodeGraphMessage::DeleteNodes { node_ids, delete_children } => {
358+
// Detect stroke proto nodes among the doomed nodes before they're gone, so the stroke-using tools'
359+
// Weight widgets can re-read the layer (they'll now read 0 px since the stroke node is missing).
360+
let any_stroke_deleted = node_ids.iter().any(|node_id| {
361+
network_interface
362+
.reference(node_id, selection_network_path)
363+
.is_some_and(|reference| reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER))
364+
});
358365
network_interface.delete_nodes(node_ids, delete_children, selection_network_path);
366+
if any_stroke_deleted {
367+
responses.add(PenToolMessage::SelectionChanged);
368+
responses.add(FreehandToolMessage::SelectionChanged);
369+
responses.add(SplineToolMessage::SelectionChanged);
370+
responses.add(ShapeToolMessage::SelectionChanged);
371+
}
359372
}
360373
// 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.
361374
// 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
17281741
}
17291742
NodeGraphMessage::SetInputValue { node_id, input_index, value } => {
17301743
let is_fill = matches!(value, TaggedValue::Fill(_));
1731-
let is_text_node = network_interface
1732-
.reference(&node_id, selection_network_path)
1733-
.is_some_and(|reference| reference == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER));
1744+
let reference = network_interface.reference(&node_id, selection_network_path);
1745+
let is_text_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER));
1746+
let is_stroke_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER));
17341747
let input = NodeInput::value(value, false);
17351748
responses.add(NodeGraphMessage::SetInput {
17361749
input_connector: InputConnector::node(node_id, input_index),
@@ -1743,6 +1756,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
17431756
if is_text_node {
17441757
responses.add(TextToolMessage::SelectionChanged);
17451758
}
1759+
if is_stroke_node {
1760+
// The dispatcher delivers each only to its tool when active, so this just covers all four stroke-using tools.
1761+
responses.add(PenToolMessage::SelectionChanged);
1762+
responses.add(FreehandToolMessage::SelectionChanged);
1763+
responses.add(SplineToolMessage::SelectionChanged);
1764+
responses.add(ShapeToolMessage::SelectionChanged);
1765+
}
17461766
if network_interface.connected_to_output(&node_id, selection_network_path) {
17471767
responses.add(NodeGraphMessage::RunDocumentGraph);
17481768
}

editor/src/messages/tool/common_functionality/graph_modification_utils.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,39 @@ pub fn get_stroke_width(layer: LayerNodeIdentifier, network_interface: &NodeNetw
493493
}
494494
}
495495

496+
/// Returns the node ID of a layer's upstream Stroke proto node, if one exists.
497+
pub fn get_stroke_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
498+
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER))
499+
}
500+
501+
/// Stroke weight of the first selected non-artboard layer, used by tool control bars to mirror the selection's weight.
502+
/// Returns `Some(0.)` if the layer has no Stroke node so the widget reads "0 px", and `None` only when no layer is selected.
503+
pub fn first_selected_stroke_weight(document: &DocumentMessageHandler) -> Option<f64> {
504+
document
505+
.network_interface
506+
.selected_nodes()
507+
.selected_layers_except_artboards(&document.network_interface)
508+
.next()
509+
.map(|layer| get_stroke_width(layer, &document.network_interface).unwrap_or(0.))
510+
}
511+
512+
/// Writes the weight back to every selected non-artboard layer's stroke. Layers with an existing stroke just have their
513+
/// `WeightInput` updated; layers without one get a fresh stroke node added (defaulting to a black stroke with the new
514+
/// weight) only when the new weight is nonzero, so changing back to 0 doesn't keep adding empty strokes.
515+
pub fn set_stroke_weight_for_selected_layers(weight: f64, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
516+
let layers: Vec<_> = document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface).collect();
517+
for layer in layers {
518+
if let Some(node_id) = get_stroke_id(layer, &document.network_interface) {
519+
let input_index = graphene_std::vector::stroke::WeightInput::INDEX;
520+
let value = TaggedValue::F64(weight);
521+
responses.add(NodeGraphMessage::SetInputValue { node_id, input_index, value });
522+
} else if weight > 0. {
523+
let stroke = graphene_std::vector::style::Stroke::default().with_weight(weight);
524+
responses.add(GraphOperationMessage::StrokeSet { layer, stroke });
525+
}
526+
}
527+
}
528+
496529
/// Checks if a specified layer uses an upstream node matching the given name.
497530
pub fn is_layer_fed_by_node_of_name(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface, identifier: &DefinitionIdentifier) -> bool {
498531
NodeGraphLayer::new(layer, network_interface).find_node_inputs(identifier).is_some()

editor/src/messages/tool/tool_messages/freehand_tool.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ pub enum FreehandToolMessage {
4444
// Standard messages
4545
Overlays { context: OverlayContext },
4646
Abort,
47+
SelectionChanged,
4748
WorkingColorChanged,
4849

4950
// Tool-specific messages
@@ -161,6 +162,16 @@ impl LayoutHolder for FreehandTool {
161162
#[message_handler_data]
162163
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for FreehandTool {
163164
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
165+
if matches!(&message, ToolMessage::Freehand(FreehandToolMessage::SelectionChanged)) {
166+
if let Some(weight) = graph_modification_utils::first_selected_stroke_weight(context.document)
167+
&& self.options.line_weight != weight
168+
{
169+
self.options.line_weight = weight;
170+
self.send_layout(responses, LayoutTarget::ToolOptions);
171+
}
172+
return;
173+
}
174+
164175
let ToolMessage::Freehand(FreehandToolMessage::UpdateOptions { options }) = message else {
165176
self.fsm_state.process_event(message, &mut self.data, context, &self.options, responses, true);
166177
return;
@@ -171,7 +182,10 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Free
171182
self.options.fill.color_type = ToolColorType::Custom;
172183
}
173184
FreehandOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type,
174-
FreehandOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight,
185+
FreehandOptionsUpdate::LineWeight(line_weight) => {
186+
self.options.line_weight = line_weight;
187+
graph_modification_utils::set_stroke_weight_for_selected_layers(line_weight, context.document, responses);
188+
}
175189
FreehandOptionsUpdate::StrokeColor(color) => {
176190
self.options.stroke.custom_color = color;
177191
self.options.stroke.color_type = ToolColorType::Custom;
@@ -208,6 +222,7 @@ impl ToolTransition for FreehandTool {
208222
EventToMessageMap {
209223
overlay_provider: Some(|context: OverlayContext| FreehandToolMessage::Overlays { context }.into()),
210224
tool_abort: Some(FreehandToolMessage::Abort.into()),
225+
selection_changed: Some(FreehandToolMessage::SelectionChanged.into()),
211226
working_color_changed: Some(FreehandToolMessage::WorkingColorChanged.into()),
212227
..Default::default()
213228
}

editor/src/messages/tool/tool_messages/pen_tool.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,14 @@ impl LayoutHolder for PenTool {
249249
#[message_handler_data]
250250
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for PenTool {
251251
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
252+
if matches!(&message, ToolMessage::Pen(PenToolMessage::SelectionChanged))
253+
&& let Some(weight) = graph_modification_utils::first_selected_stroke_weight(context.document)
254+
&& self.options.line_weight != weight
255+
{
256+
self.options.line_weight = weight;
257+
self.send_layout(responses, LayoutTarget::ToolOptions);
258+
}
259+
252260
let ToolMessage::Pen(PenToolMessage::UpdateOptions { options }) = message else {
253261
self.fsm_state.process_event(message, &mut self.tool_data, context, &self.options, responses, true);
254262
return;
@@ -259,7 +267,10 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for PenT
259267
self.options.pen_overlay_mode = overlay_mode_type;
260268
responses.add(OverlaysMessage::Draw);
261269
}
262-
PenOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight,
270+
PenOptionsUpdate::LineWeight(line_weight) => {
271+
self.options.line_weight = line_weight;
272+
graph_modification_utils::set_stroke_weight_for_selected_layers(line_weight, context.document, responses);
273+
}
263274
PenOptionsUpdate::FillColor(color) => {
264275
self.options.fill.custom_color = color;
265276
self.options.fill.color_type = ToolColorType::Custom;

editor/src/messages/tool/tool_messages/shape_tool.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ pub enum ShapeToolMessage {
9595
// Standard messages
9696
Overlays { context: OverlayContext },
9797
Abort,
98+
SelectionChanged,
9899
WorkingColorChanged,
99100

100101
// Tool-specific messages
@@ -415,6 +416,16 @@ impl LayoutHolder for ShapeTool {
415416
#[message_handler_data]
416417
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for ShapeTool {
417418
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
419+
if matches!(&message, ToolMessage::Shape(ShapeToolMessage::SelectionChanged)) {
420+
if let Some(weight) = graph_modification_utils::first_selected_stroke_weight(context.document)
421+
&& self.options.line_weight != weight
422+
{
423+
self.options.line_weight = weight;
424+
self.send_layout(responses, LayoutTarget::ToolOptions);
425+
}
426+
return;
427+
}
428+
418429
let ToolMessage::Shape(ShapeToolMessage::UpdateOptions { options }) = message else {
419430
self.fsm_state.process_event(message, &mut self.tool_data, context, &self.options, responses, true);
420431
return;
@@ -429,6 +440,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Shap
429440
}
430441
ShapeOptionsUpdate::LineWeight(line_weight) => {
431442
self.options.line_weight = line_weight;
443+
graph_modification_utils::set_stroke_weight_for_selected_layers(line_weight, context.document, responses);
432444
}
433445
ShapeOptionsUpdate::StrokeColor(color) => {
434446
self.options.stroke.custom_color = color;
@@ -527,6 +539,7 @@ impl ToolTransition for ShapeTool {
527539
EventToMessageMap {
528540
overlay_provider: Some(|context| ShapeToolMessage::Overlays { context }.into()),
529541
tool_abort: Some(ShapeToolMessage::Abort.into()),
542+
selection_changed: Some(ShapeToolMessage::SelectionChanged.into()),
530543
working_color_changed: Some(ShapeToolMessage::WorkingColorChanged.into()),
531544
..Default::default()
532545
}

editor/src/messages/tool/tool_messages/spline_tool.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ pub enum SplineToolMessage {
4646
Overlays { context: OverlayContext },
4747
CanvasTransformed,
4848
Abort,
49+
SelectionChanged,
4950
WorkingColorChanged,
5051

5152
// Tool-specific messages
@@ -168,12 +169,25 @@ impl LayoutHolder for SplineTool {
168169
#[message_handler_data]
169170
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for SplineTool {
170171
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
172+
if matches!(&message, ToolMessage::Spline(SplineToolMessage::SelectionChanged)) {
173+
if let Some(weight) = graph_modification_utils::first_selected_stroke_weight(context.document)
174+
&& self.options.line_weight != weight
175+
{
176+
self.options.line_weight = weight;
177+
self.send_layout(responses, LayoutTarget::ToolOptions);
178+
}
179+
return;
180+
}
181+
171182
let ToolMessage::Spline(SplineToolMessage::UpdateOptions { options }) = message else {
172183
self.fsm_state.process_event(message, &mut self.tool_data, context, &self.options, responses, true);
173184
return;
174185
};
175186
match options {
176-
SplineOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight,
187+
SplineOptionsUpdate::LineWeight(line_weight) => {
188+
self.options.line_weight = line_weight;
189+
graph_modification_utils::set_stroke_weight_for_selected_layers(line_weight, context.document, responses);
190+
}
177191
SplineOptionsUpdate::FillColor(color) => {
178192
self.options.fill.custom_color = color;
179193
self.options.fill.color_type = ToolColorType::Custom;
@@ -224,8 +238,8 @@ impl ToolTransition for SplineTool {
224238
overlay_provider: Some(|context: OverlayContext| SplineToolMessage::Overlays { context }.into()),
225239
canvas_transformed: Some(SplineToolMessage::CanvasTransformed.into()),
226240
tool_abort: Some(SplineToolMessage::Abort.into()),
241+
selection_changed: Some(SplineToolMessage::SelectionChanged.into()),
227242
working_color_changed: Some(SplineToolMessage::WorkingColorChanged.into()),
228-
..Default::default()
229243
}
230244
}
231245
}

editor/src/messages/tool/tool_messages/text_tool.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,9 @@ impl ToolMetadata for TextTool {
9999
}
100100

101101
fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog, document: &DocumentMessageHandler) -> Vec<WidgetInstance> {
102-
// If a single text layer is selected, the toolbar's font/style menus drive that layer's text node directly, going through the
102+
// If a single text layer is selected, the control bar's font/style menus drive that layer's text node directly, going through the
103103
// same code path as the Properties panel (LoadFontData + SetInputValue, with closest_style and font_style_to_restore bookkeeping).
104-
// Otherwise the menus only update the toolbar option for the next created text.
104+
// Otherwise the menus only update the control bar option for the next created text.
105105
let text_node_id = can_edit_selected(document).and_then(|layer| graph_modification_utils::get_text_id(layer, &document.network_interface));
106106

107107
let font_input_index = graphene_std::text::text::FontInput::INDEX;
@@ -324,8 +324,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
324324
};
325325
match options {
326326
TextOptionsUpdate::Font { font } => {
327-
// The toolbar font/style menus go through `SetInputValue` directly when a text layer is selected, so this
328-
// arm only fires when no layer is selected (toolbar font is just the default for the next-created text).
327+
// The control bar font/style menus go through `SetInputValue` directly when a text layer is selected, so this
328+
// arm only fires when no layer is selected (control bar font is just the default for the next-created text).
329329
self.options.font = font.clone();
330330
if let Some(editing_text) = self.tool_data.editing_text.as_mut() {
331331
editing_text.font = font;

0 commit comments

Comments
 (0)