Fix style controls corrupting stack nodes like Transform that can't host blending or paint nodes (#4428)

* Stop chain node insertion from stranding a node on layers without a chain

* Gate the Layers panel blending controls on whether the layer can host them

* Gate the fill and stroke setters on whether the layer can host paint nodes

* Make the tool options fill and stroke controls treat layers that cannot take paint as unselected

* Remove the unused first_selected_stroke_weight helper
This commit is contained in:
Keavon Chambers
2026-08-11 12:48:57 -07:00
committed by Dennis Kobert
parent 0e0687c8cb
commit ae1ff71f45
8 changed files with 108 additions and 68 deletions

View File

@@ -3409,6 +3409,11 @@ impl DocumentMessageHandler {
let selected_nodes = self.network_interface.selected_nodes();
let selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&self.network_interface);
// A layer whose chain cannot carry blending nodes has nowhere to put the value, so it disqualifies the whole selection
let all_layers_support_blending = selected_nodes
.selected_layers_except_artboards(&self.network_interface)
.all(|layer| self.network_interface.layer_hosts_blending_nodes(&layer.to_node(), &[]));
// Look up the current opacity and blend mode of the selected layers (if any), and split the iterator into the first tuple and the rest.
let mut blending_options = selected_layers_except_artboards.map(|layer| {
(
@@ -3420,8 +3425,8 @@ impl DocumentMessageHandler {
let first_blending_options = blending_options.next();
let result_blending_options = blending_options;
// If there are no selected layers, disable the opacity and blend mode widgets.
let disabled = first_blending_options.is_none();
// If there are no selected layers, or any of them cannot host the nodes, disable the opacity and blend mode widgets.
let disabled = first_blending_options.is_none() || !all_layers_support_blending;
// Amongst the selected layers, check if the opacities and blend modes are identical across all layers.
// The result is setting `option` and `blend_mode` to Some value if all their values are identical, or None if they are not.

View File

@@ -348,6 +348,16 @@ impl<'a> ModifyInputsContext<'a> {
self.existing_node_id(&DefinitionIdentifier::ProtoNode(reference), create_if_nonexistent)
}
/// The same as [`Self::existing_proto_node_id`], but yielding `None` on layers whose chain cannot host the node.
fn existing_chain_hosted_node_id(&mut self, reference: ProtoNodeIdentifier, create_if_nonexistent: bool) -> Option<NodeId> {
let output_layer = self.get_output_layer()?;
if !self.network_interface.layer_chain_hosts_node(&output_layer.to_node(), &[], &reference) {
return None;
}
self.existing_proto_node_id(reference, create_if_nonexistent)
}
/// Gets the node id of a document node with a specific reference that is upstream from the layer node, and optionally creates it if it does not exist.
fn existing_node_id(&mut self, reference: &DefinitionIdentifier, create_if_nonexistent: bool) -> Option<NodeId> {
// Start from the layer node or export
@@ -398,6 +408,9 @@ impl<'a> ModifyInputsContext<'a> {
return None;
};
// Without a secondary input there is no chain to hold the node, so inserting it would strand it at the graph origin
self.network_interface.input_from_connector(&InputConnector::layer_secondary_input(output_layer.to_node()), &[])?;
// If inserting a 'Path' node, insert a 'Combine Paths' node if the type is `Graphic`.
// TODO: Allow the 'Path' node to operate on `List` data by utilizing the reference (index or ID?) for each item.
if node_definition.identifier == "Path" {
@@ -419,7 +432,7 @@ impl<'a> ModifyInputsContext<'a> {
}
pub fn fill_color_set(&mut self, color: Option<Color>) {
let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
let Some(fill_node_id) = self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
return;
};
let input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput);
@@ -434,7 +447,7 @@ impl<'a> ModifyInputsContext<'a> {
}
pub fn fill_gradient_set(&mut self, gradient: Gradient, gradient_form: GradientForm, settings: GradientSettings, transform: DAffine2) {
let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
let Some(fill_node_id) = self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
return;
};
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput);
@@ -478,7 +491,7 @@ impl<'a> ModifyInputsContext<'a> {
}
pub fn blend_mode_set(&mut self, blend_mode: BlendMode) {
let Some(blend_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::blend_mode::IDENTIFIER, true) else {
let Some(blend_node_id) = self.existing_chain_hosted_node_id(graphene_std::blending_nodes::blend_mode::IDENTIFIER, true) else {
return;
};
let input_connector = InputConnector::node(blend_node_id, graphene_std::blending_nodes::blend_mode::BlendModeInput);
@@ -486,7 +499,7 @@ impl<'a> ModifyInputsContext<'a> {
}
pub fn opacity_set(&mut self, opacity: f64) {
let Some(opacity_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::opacity::IDENTIFIER, true) else {
let Some(opacity_node_id) = self.existing_chain_hosted_node_id(graphene_std::blending_nodes::opacity::IDENTIFIER, true) else {
return;
};
// Enable the `has_opacity` checkbox so the value is applied
@@ -505,9 +518,9 @@ impl<'a> ModifyInputsContext<'a> {
pub fn opacity_fill_set(&mut self, fill: f64) {
// Reuse an existing Opacity node to avoid a redundant chain walk on slider drags
let identifier = graphene_std::blending_nodes::opacity::IDENTIFIER;
let existing = self.existing_proto_node_id(identifier.clone(), false);
let existing = self.existing_chain_hosted_node_id(identifier.clone(), false);
let existed = existing.is_some();
let Some(opacity_node_id) = existing.or_else(|| self.existing_proto_node_id(identifier, true)) else {
let Some(opacity_node_id) = existing.or_else(|| self.existing_chain_hosted_node_id(identifier, true)) else {
return;
};
// Freshly-created node defaults to opacity enabled; disable it so the fill slider works independently
@@ -821,7 +834,7 @@ impl<'a> ModifyInputsContext<'a> {
pub fn clip_mode_toggle(&mut self, clip_mode: Option<bool>) {
let clip = !clip_mode.unwrap_or(false);
let Some(clip_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::clipping_mask::IDENTIFIER, true) else {
let Some(clip_node_id) = self.existing_chain_hosted_node_id(graphene_std::blending_nodes::clipping_mask::IDENTIFIER, true) else {
return;
};
let input_connector = InputConnector::node(clip_node_id, graphene_std::blending_nodes::clipping_mask::ClipInput);
@@ -829,7 +842,7 @@ impl<'a> ModifyInputsContext<'a> {
}
pub fn stroke_set(&mut self, color: Option<Color>, stroke: Stroke) {
let Some(stroke_node_id) = self.existing_proto_node_id(graphene_std::vector::stroke::IDENTIFIER, true) else {
let Some(stroke_node_id) = self.existing_chain_hosted_node_id(graphene_std::vector::stroke::IDENTIFIER, true) else {
return;
};

View File

@@ -2853,7 +2853,7 @@ impl NodeGraphMessageHandler {
}))
);
let clippable = layer.can_be_clipped(network_interface.document_metadata());
let clippable = layer.can_be_clipped(network_interface.document_metadata()) && network_interface.layer_hosts_blending_nodes(&node_id, &[]);
let data = LayerPanelEntry {
id: node_id,

View File

@@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNodeImplementation, InlineRust, NodeInput};
use graph_craft::proto::{GraphErrorType, GraphErrors};
use graph_craft::{Type, concrete};
use graph_craft::{ProtoNodeIdentifier, Type, concrete};
use graphene_std::uuid::NodeId;
use interpreted_executor::dynamic_executor::{NodeTypes, ResolvedDocumentNodeTypesDelta};
use interpreted_executor::node_registry::NODE_REGISTRY;
@@ -174,6 +174,39 @@ impl NodeNetworkInterface {
}
}
/// Whether the given node has a registered implementation accepting the layer chain's element type as its content input.
/// A chain awaiting compilation has no resolved type yet, so only a known-wrong type or a type error locks the layer out.
pub fn layer_chain_hosts_node(&self, node_id: &NodeId, network_path: &[NodeId], node: &ProtoNodeIdentifier) -> bool {
let secondary_input = InputConnector::layer_secondary_input(*node_id);
if !self.input_from_connector(&secondary_input, network_path).is_some_and(|input| input.is_exposed()) {
return false;
}
let chain_type = self.input_type(&secondary_input, network_path);
match chain_type.compiled_nested_type() {
Some(element) => {
let Some(implementations) = NODE_REGISTRY.get(node) else {
log::error!("Proto node {node:?} not found in the node registry, in layer_chain_hosts_node");
return false;
};
implementations.iter().any(|entry| entry.io.inputs.first().is_some_and(|content| content.nested_type() == element))
}
None => !matches!(chain_type, TypeSource::Invalid),
}
}
/// Whether the blending nodes (blend mode, opacity, clipping mask) can be spliced into this layer's chain.
pub fn layer_hosts_blending_nodes(&self, node_id: &NodeId, network_path: &[NodeId]) -> bool {
// Blend Mode stands in for the trio since they share one implementations list
self.layer_chain_hosts_node(node_id, network_path, &graphene_std::blending_nodes::blend_mode::IDENTIFIER)
}
/// Whether the Fill and Stroke nodes can be spliced into this layer's chain.
pub fn layer_hosts_paint_nodes(&self, node_id: &NodeId, network_path: &[NodeId]) -> bool {
// Fill stands in for both since they share one implementations list
self.layer_chain_hosts_node(node_id, network_path, &graphene_std::vector_nodes::fill::IDENTIFIER)
}
/// Get the [`TypeSource`] for any InputConnector.
/// If the input is not compiled, then an Unknown or default from the definition is returned.
pub fn input_type(&self, input_connector: &InputConnector, network_path: &[NodeId]) -> TypeSource {

View File

@@ -304,10 +304,8 @@ pub fn sync_drawing_state(drawing: &mut DrawingToolState, natural_fill_enabled:
/// Reads the stroke proto-node inputs (align, cap, join, miter limit, paint order, dash lengths, dash offset) across the selection and updates
/// the matching fields on `drawing`. Each field becomes `None` (mixed) when selected strokes disagree. With no selection, fields are left as-is.
fn sync_stroke_options(drawing: &mut DrawingToolState, document: &DocumentMessageHandler) -> bool {
let strokes: Vec<_> = document
.network_interface
.selected_nodes()
.selected_layers_except_artboards(&document.network_interface)
let strokes: Vec<_> = graph_modification_utils::paintable_selected_layers(document)
.into_iter()
.filter_map(|layer| graph_modification_utils::get_stroke_options(layer, &document.network_interface))
.collect();
if strokes.is_empty() {
@@ -391,14 +389,9 @@ pub fn sync_fill_only(fill: &mut ToolColorOptions, natural_fill_enabled: bool, f
}
}
/// True if at least one (non-artboard) layer is currently selected.
pub fn has_selection(document: &DocumentMessageHandler) -> bool {
document
.network_interface
.selected_nodes()
.selected_layers_except_artboards(&document.network_interface)
.next()
.is_some()
/// True if at least one selected layer can take paint, making the swatches edit the selection instead of the tool's own colors.
pub fn has_paintable_selection(document: &DocumentMessageHandler) -> bool {
!graph_modification_utils::paintable_selected_layers(document).is_empty()
}
/// Applies a user-picked fill (gradient or solid). With a selection, writes to the layers; with none, pushes a solid to the swap-routed working color slot.
@@ -411,7 +404,7 @@ pub fn apply_fill_only_color_pick(fill: &mut ToolColorOptions, fill_choice: Fill
fill.fill_choice = Some(fill_choice.clone());
fill.enabled = Some(true);
fill.tracks_working_color = false;
if has_selection(document) {
if has_paintable_selection(document) {
if document.network_interface.transaction_status() == TransactionStatus::Finished {
responses.add(DocumentMessage::StartTransaction);
}
@@ -426,7 +419,7 @@ pub fn apply_stroke_color_pick(drawing: &mut DrawingToolState, color: Option<Col
drawing.stroke.fill_choice = Some(color.map_or(FillChoice::None, FillChoice::Solid));
drawing.stroke.enabled = Some(true);
drawing.stroke.tracks_working_color = false;
if has_selection(document) {
if has_paintable_selection(document) {
if document.network_interface.transaction_status() == TransactionStatus::Finished {
responses.add(DocumentMessage::StartTransaction);
}
@@ -447,7 +440,7 @@ pub fn apply_fill_enabled(drawing: &mut DrawingToolState, enabled: bool, global:
/// Single-slot variant of [`apply_fill_enabled`]. `working_color` is the fallback used when re-ticking or unticking from a mixed state.
pub fn apply_fill_only_enabled(fill: &mut ToolColorOptions, enabled: bool, working_color: Color, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
fill.enabled = Some(enabled);
if has_selection(document) {
if has_paintable_selection(document) {
responses.add(DocumentMessage::AddTransaction);
}
if enabled {
@@ -471,7 +464,7 @@ pub fn apply_fill_only_enabled(fill: &mut ToolColorOptions, enabled: bool, worki
/// Toggles the stroke checkbox: mirrors [`apply_fill_enabled`].
pub fn apply_stroke_enabled(drawing: &mut DrawingToolState, enabled: bool, global: &DocumentToolData, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
drawing.stroke.enabled = Some(enabled);
if has_selection(document) {
if has_paintable_selection(document) {
responses.add(DocumentMessage::AddTransaction);
}
if enabled {
@@ -493,7 +486,7 @@ pub fn apply_stroke_enabled(drawing: &mut DrawingToolState, enabled: bool, globa
/// Applies a user-edited stroke weight to the selection, also persisting it as the no-selection default.
pub fn apply_line_weight(drawing: &mut DrawingToolState, line_weight: f64, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
drawing.line_weight = Some(line_weight);
if !has_selection(document) {
if !has_paintable_selection(document) {
drawing.default_line_weight = line_weight;
}
graph_modification_utils::set_stroke_weight_for_selected_layers(line_weight, document, responses);
@@ -507,7 +500,7 @@ pub fn apply_working_colors(drawing: &mut DrawingToolState, global: &DocumentToo
/// Refreshes a single swatch from the given working color, subject to the rules in [`apply_working_colors`].
pub fn refresh_slot_working_color(slot: &mut ToolColorOptions, working_color: Color, document: &DocumentMessageHandler) {
if slot.fill_choice.is_some() && (!has_selection(document) || slot.tracks_working_color) {
if slot.fill_choice.is_some() && (!has_paintable_selection(document) || slot.tracks_working_color) {
slot.fill_choice = Some(solid(working_color));
}
}
@@ -524,7 +517,7 @@ pub fn reset_colors_on_deactivation(drawing: &mut DrawingToolState, global: &Doc
pub fn swap_fill_and_stroke(drawing: &mut DrawingToolState, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
drawing.colors_swapped = !drawing.colors_swapped;
if has_selection(document) {
if has_paintable_selection(document) {
responses.add(DocumentMessage::AddTransaction);
}
@@ -539,7 +532,7 @@ pub fn swap_fill_and_stroke(drawing: &mut DrawingToolState, document: &DocumentM
drawing.fill.tracks_working_color = new_fill_tracks;
drawing.stroke.tracks_working_color = new_stroke_tracks;
if has_selection(document) {
if has_paintable_selection(document) {
// Apply to layers only when we have a concrete value (`None` means mixed, no single value to broadcast).
if drawing.fill.is_active()
&& let Some(choice) = new_fill
@@ -579,7 +572,7 @@ pub enum WeightSyncOutcome {
/// Inspects the selection and returns how the weight widget should update.
pub fn compute_weight_sync(document: &DocumentMessageHandler) -> WeightSyncOutcome {
let layers: Vec<_> = document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface).collect();
let layers = graph_modification_utils::paintable_selected_layers(document);
if layers.is_empty() {
return WeightSyncOutcome::NoSelection;

View File

@@ -721,23 +721,11 @@ pub fn get_stroke_id(layer: LayerNodeIdentifier, network_interface: &NodeNetwork
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 {
for layer in paintable_selected_layers(document) {
if let Some(node_id) = get_stroke_id(layer, &document.network_interface) {
responses.add(NodeGraphMessage::SetInputValue {
node_id,
@@ -817,12 +805,21 @@ pub struct SelectedStrokeState {
pub optional_color: Option<Option<Color>>,
}
/// The selected layers (artboards excluded) whose chains can host Fill and Stroke nodes.
/// The tool options bar's paint widgets treat these as the whole selection, so other layers act as if unselected.
pub fn paintable_selected_layers(document: &DocumentMessageHandler) -> Vec<LayerNodeIdentifier> {
let selected_nodes = document.network_interface.selected_nodes();
selected_nodes
.selected_layers_except_artboards(&document.network_interface)
.filter(|layer| document.network_interface.layer_hosts_paint_nodes(&layer.to_node(), &[]))
.collect()
}
/// Reads the fill state across all selected non-artboard layers, including whether their enabled states or colors differ.
/// "Enabled" tracks node attachment: a layer counts as enabled whenever a Fill node is attached, even when that fill's value is the no-paint choice.
/// Unticked means there is no Fill node. Returns `None` only when no layer is selected.
/// Unticked means there is no Fill node. Returns `None` only when no paintable layer is selected.
pub fn selected_fill_state(document: &DocumentMessageHandler) -> Option<SelectedFillState> {
let selected_nodes = document.network_interface.selected_nodes();
let mut per_layer = selected_nodes.selected_layers_except_artboards(&document.network_interface).map(|layer| {
let mut per_layer = paintable_selected_layers(document).into_iter().map(|layer| {
let Some(fill_node_id) = get_fill_id(layer, &document.network_interface) else {
return (false, FillChoice::None);
};
@@ -873,8 +870,7 @@ pub fn selected_fill_state(document: &DocumentMessageHandler) -> Option<Selected
/// "Enabled" tracks node attachment: a layer counts as enabled whenever a Stroke node is attached, even when that stroke's color is `None`.
/// Unticked means there is no Stroke node. Returns `None` only when no layer is selected.
pub fn selected_stroke_state(document: &DocumentMessageHandler) -> Option<SelectedStrokeState> {
let selected_nodes = document.network_interface.selected_nodes();
let mut per_layer = selected_nodes.selected_layers_except_artboards(&document.network_interface).map(|layer| {
let mut per_layer = paintable_selected_layers(document).into_iter().map(|layer| {
if get_stroke_id(layer, &document.network_interface).is_none() {
return (false, None);
}
@@ -911,8 +907,7 @@ pub fn selected_stroke_state(document: &DocumentMessageHandler) -> Option<Select
/// Sets the fill on all selected non-artboard layers, preserving gradient transform data when the layer already has a gradient fill.
pub fn set_fill_for_selected_layers(fill_choice: FillChoice, 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 {
for layer in paintable_selected_layers(document) {
match &fill_choice {
FillChoice::None => responses.add(GraphOperationMessage::FillColorSet { layer, color: None }),
FillChoice::Solid(color) => responses.add(GraphOperationMessage::FillColorSet { layer, color: Some(*color) }),
@@ -947,8 +942,7 @@ pub fn set_fill_for_selected_layers(fill_choice: FillChoice, document: &Document
/// the provided `weight`, so picking any color (including `None`) from an unticked stroke control bar entry both attaches
/// the Stroke node and applies the chosen color.
pub fn set_stroke_color_for_selected_layers(color: Option<Color>, 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 {
for layer in paintable_selected_layers(document) {
if let Some(node_id) = get_stroke_id(layer, &document.network_interface) {
responses.add(NodeGraphMessage::SetInputValue {
node_id,
@@ -964,8 +958,7 @@ pub fn set_stroke_color_for_selected_layers(color: Option<Color>, weight: f64, d
/// Removes the Fill node from all selected non-artboard layers.
pub fn remove_fill_for_selected_layers(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 {
for layer in paintable_selected_layers(document) {
if let Some(node_id) = get_fill_id(layer, &document.network_interface) {
responses.add(NodeGraphMessage::DeleteNodes {
node_ids: vec![node_id],
@@ -979,8 +972,7 @@ pub fn remove_fill_for_selected_layers(document: &DocumentMessageHandler, respon
/// Removes the Stroke node from all selected non-artboard layers.
pub fn remove_stroke_for_selected_layers(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 {
for layer in paintable_selected_layers(document) {
if let Some(node_id) = get_stroke_id(layer, &document.network_interface) {
responses.add(NodeGraphMessage::DeleteNodes {
node_ids: vec![node_id],

View File

@@ -12,7 +12,7 @@ use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes;
use crate::messages::preferences::SelectionMode;
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::color_selector::{
DrawingToolState, apply_fill_color_pick, apply_fill_enabled, apply_stroke_color_pick, apply_stroke_enabled, apply_working_colors, swap_fill_and_stroke, sync_drawing_state,
DrawingToolState, apply_fill_color_pick, apply_fill_enabled, apply_stroke_color_pick, apply_stroke_enabled, apply_working_colors, has_paintable_selection, swap_fill_and_stroke, sync_drawing_state,
};
use crate::messages::tool::common_functionality::compass_rose::{Axis, CompassRose};
use crate::messages::tool::common_functionality::graph_modification_utils;
@@ -236,8 +236,8 @@ impl LayoutHolder for SelectTool {
fn layout(&self) -> Layout {
let mut widgets = Vec::new();
// Fill/Stroke widget set (only shown when there's a selection to apply edits to)
if self.tool_data.selected_layers_count > 0 {
// Fill/Stroke widget set (only shown when there's a paintable selection to apply edits to)
if self.tool_data.paintable_layers_selected {
widgets.append(&mut self.drawing.fill.create_widgets(
"Fill:",
|checkbox: &CheckboxInput| {
@@ -509,6 +509,8 @@ struct SelectToolData {
skew_edge: EdgeBool,
nested_selection_behavior: NestedSelectionBehavior,
selected_layers_count: usize,
/// Whether any selected layer can take paint, controlling the visibility of the fill/stroke widget row.
paintable_layers_selected: bool,
selected_layers_changed: bool,
snap_candidates: Vec<SnapCandidatePoint>,
auto_panning: AutoPanning,
@@ -738,8 +740,10 @@ impl Fsm for SelectToolFsmState {
crate::messages::tool::common_functionality::layer_origin_cross::draw_for_selected_layers(&mut overlay_context, document);
let selected_layers_count = document.network_interface.selected_nodes().selected_unlocked_layers(&document.network_interface).count();
tool_data.selected_layers_changed = selected_layers_count != tool_data.selected_layers_count;
let paintable_layers_selected = has_paintable_selection(document);
tool_data.selected_layers_changed = selected_layers_count != tool_data.selected_layers_count || paintable_layers_selected != tool_data.paintable_layers_selected;
tool_data.selected_layers_count = selected_layers_count;
tool_data.paintable_layers_selected = paintable_layers_selected;
// Outline selected layers, but not artboards
if overlay_context.visibility_settings.selection_outline() {

View File

@@ -6,7 +6,7 @@ use crate::messages::portfolio::document::overlays::utility_types::OverlayContex
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::color_selector::{
DrawingToolState, apply_fill_color_pick, apply_fill_enabled, apply_stroke_color_pick, apply_stroke_enabled, apply_working_colors, has_selection, reset_colors_on_deactivation,
DrawingToolState, apply_fill_color_pick, apply_fill_enabled, apply_stroke_color_pick, apply_stroke_enabled, apply_working_colors, has_paintable_selection, reset_colors_on_deactivation,
swap_fill_and_stroke, sync_color_options, sync_drawing_state,
};
use crate::messages::tool::common_functionality::gizmos::gizmo_manager::GizmoManager;
@@ -582,8 +582,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Shap
apply_fill_color_pick(&mut self.options.drawing, fill_choice, context.document, responses);
}
ShapeOptionsUpdate::FillEnabled(enabled) => {
// When toggled with no selection, persist the new state as the current shape mode's default
if !has_selection(context.document) {
// When toggled with no paintable selection, persist the new state as the current shape mode's default
if !has_paintable_selection(context.document) {
self.options.shape_fill_defaults.insert(self.tool_data.current_shape, enabled);
}
apply_fill_enabled(&mut self.options.drawing, enabled, context.global_tool_data, context.document, responses);
@@ -595,8 +595,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Shap
apply_stroke_color_pick(&mut self.options.drawing, color, context.document, responses);
}
ShapeOptionsUpdate::StrokeEnabled(enabled) => {
// When toggled with no selection, persist the new state as the current shape mode's default
if !has_selection(context.document) {
// When toggled with no paintable selection, persist the new state as the current shape mode's default
if !has_paintable_selection(context.document) {
self.options.shape_stroke_defaults.insert(self.tool_data.current_shape, enabled);
}
apply_stroke_enabled(&mut self.options.drawing, enabled, context.global_tool_data, context.document, responses);