Redesign how the control bar handles fill and stroke colors (#4137)
* Revamp how the control bar handles fill and stroke colors * Fix bugs * Code review
This commit is contained in:
parent
f6def3b911
commit
629a1f4b4c
|
|
@ -102,6 +102,8 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
|
|||
self.gradient = None;
|
||||
self.active_marker_index = None;
|
||||
self.active_marker_is_midpoint = false;
|
||||
|
||||
responses.add(DocumentMessage::EndTransaction);
|
||||
}
|
||||
ColorPickerMessage::VisualUpdate { update } => {
|
||||
self.hue = update.hue;
|
||||
|
|
|
|||
|
|
@ -202,6 +202,13 @@ pub struct ColorInput {
|
|||
#[serde(rename = "menuDirection")]
|
||||
pub menu_direction: Option<MenuDirection>,
|
||||
pub disabled: bool,
|
||||
pub mixed: bool,
|
||||
|
||||
// Sizing
|
||||
#[serde(rename = "minWidth")]
|
||||
pub min_width: u32,
|
||||
#[serde(rename = "maxWidth")]
|
||||
pub max_width: u32,
|
||||
|
||||
// Styling
|
||||
pub narrow: bool,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ pub struct CheckboxInput {
|
|||
#[serde(rename = "forLabel")]
|
||||
pub for_label: CheckboxId,
|
||||
pub disabled: bool,
|
||||
pub mixed: bool,
|
||||
|
||||
// Tooltips
|
||||
#[serde(rename = "tooltipLabel")]
|
||||
|
|
|
|||
|
|
@ -323,7 +323,6 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||
let layer = modify_inputs.create_layer(id);
|
||||
modify_inputs.insert_text(text, font, typesetting, layer);
|
||||
network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
|
||||
responses.add(GraphOperationMessage::StrokeSet { layer, stroke: Stroke::default() });
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
GraphOperationMessage::ResizeArtboard { layer, location, dimensions } => {
|
||||
|
|
|
|||
|
|
@ -267,9 +267,6 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
let transform = resolve_proto_node_type(graphene_std::transform_nodes::transform::IDENTIFIER)
|
||||
.expect("Transform node does not exist")
|
||||
.default_node_template();
|
||||
let stroke = resolve_proto_node_type(graphene_std::vector_nodes::stroke::IDENTIFIER)
|
||||
.expect("Stroke node does not exist")
|
||||
.default_node_template();
|
||||
let fill = resolve_proto_node_type(graphene_std::vector_nodes::fill::IDENTIFIER)
|
||||
.expect("Fill node does not exist")
|
||||
.default_node_template();
|
||||
|
|
@ -282,10 +279,6 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
self.network_interface.insert_node(transform_id, transform, &[]);
|
||||
self.network_interface.move_node_to_chain_start(&transform_id, layer, &[], self.import);
|
||||
|
||||
let stroke_id = NodeId::new();
|
||||
self.network_interface.insert_node(stroke_id, stroke, &[]);
|
||||
self.network_interface.move_node_to_chain_start(&stroke_id, layer, &[], self.import);
|
||||
|
||||
let fill_id = NodeId::new();
|
||||
self.network_interface.insert_node(fill_id, fill, &[]);
|
||||
self.network_interface.move_node_to_chain_start(&fill_id, layer, &[], self.import);
|
||||
|
|
|
|||
|
|
@ -355,19 +355,21 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> 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| {
|
||||
// Detect stroke/fill proto nodes among the doomed nodes before they're gone so the tool control bars can re-sync
|
||||
let stroke = DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER);
|
||||
let fill = DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER);
|
||||
let any_fill_or_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))
|
||||
.is_some_and(|reference| reference == stroke || reference == fill)
|
||||
});
|
||||
network_interface.delete_nodes(node_ids, delete_children, selection_network_path);
|
||||
if any_stroke_deleted {
|
||||
if any_fill_or_stroke_deleted {
|
||||
responses.add(PenToolMessage::SelectionChanged);
|
||||
responses.add(FreehandToolMessage::SelectionChanged);
|
||||
responses.add(SplineToolMessage::SelectionChanged);
|
||||
responses.add(ShapeToolMessage::SelectionChanged);
|
||||
responses.add(TextToolMessage::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.
|
||||
|
|
@ -1740,21 +1742,17 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||
}
|
||||
}
|
||||
NodeGraphMessage::SetInputValue { node_id, input_index, value } => {
|
||||
use graphene_std::vector::generator_nodes::*;
|
||||
|
||||
let is_fill = matches!(value, TaggedValue::Fill(_));
|
||||
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 is_fill_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER));
|
||||
let is_shape_generator_node = reference.as_ref().is_some_and(|r| {
|
||||
[
|
||||
graphene_std::vector::generator_nodes::regular_polygon::IDENTIFIER,
|
||||
graphene_std::vector::generator_nodes::star::IDENTIFIER,
|
||||
graphene_std::vector::generator_nodes::arc::IDENTIFIER,
|
||||
graphene_std::vector::generator_nodes::spiral::IDENTIFIER,
|
||||
graphene_std::vector::generator_nodes::grid::IDENTIFIER,
|
||||
graphene_std::vector::generator_nodes::arrow::IDENTIFIER,
|
||||
]
|
||||
.into_iter()
|
||||
.any(|id| *r == DefinitionIdentifier::ProtoNode(id))
|
||||
[regular_polygon::IDENTIFIER, star::IDENTIFIER, arc::IDENTIFIER, spiral::IDENTIFIER, grid::IDENTIFIER, arrow::IDENTIFIER]
|
||||
.into_iter()
|
||||
.any(|id| *r == DefinitionIdentifier::ProtoNode(id))
|
||||
});
|
||||
|
||||
let input = NodeInput::value(value, false);
|
||||
|
|
@ -1766,15 +1764,12 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||
if is_fill {
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
if is_text_node {
|
||||
responses.add(TextToolMessage::SelectionChanged);
|
||||
}
|
||||
if is_stroke_node || is_shape_generator_node {
|
||||
// The dispatcher delivers each only to its tool when active, so this just covers all four stroke-using tools.
|
||||
if is_stroke_node || is_fill_node || is_shape_generator_node || is_text_node {
|
||||
responses.add(PenToolMessage::SelectionChanged);
|
||||
responses.add(FreehandToolMessage::SelectionChanged);
|
||||
responses.add(SplineToolMessage::SelectionChanged);
|
||||
responses.add(ShapeToolMessage::SelectionChanged);
|
||||
responses.add(TextToolMessage::SelectionChanged);
|
||||
}
|
||||
if network_interface.connected_to_output(&node_id, selection_network_path) {
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
|
|
|
|||
|
|
@ -1,74 +1,77 @@
|
|||
use crate::consts::DEFAULT_STROKE_WIDTH;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::TransactionStatus;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::utility_types::DocumentToolData;
|
||||
use graphene_std::Color;
|
||||
use graphene_std::vector::style::FillChoice;
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ToolColorType {
|
||||
Primary,
|
||||
Secondary,
|
||||
Custom,
|
||||
}
|
||||
|
||||
/// Color selector widgets seen in [`LayoutTarget::ToolOptions`] bar.
|
||||
pub struct ToolColorOptions {
|
||||
pub custom_color: Option<Color>,
|
||||
pub primary_working_color: Option<Color>,
|
||||
pub secondary_working_color: Option<Color>,
|
||||
pub color_type: ToolColorType,
|
||||
/// The fill/stroke value shown in the swatch. `None` = mixed across selected layers.
|
||||
pub fill_choice: Option<FillChoice>,
|
||||
/// The checkbox state. `None` = mixed across selected layers.
|
||||
pub enabled: Option<bool>,
|
||||
/// When set, `fill_choice` is a working-color fallback (vs. a layer-derived saved color) and is refreshed live by `WorkingColorChanged`.
|
||||
pub tracks_working_color: bool,
|
||||
}
|
||||
|
||||
impl Default for ToolColorOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
color_type: ToolColorType::Primary,
|
||||
custom_color: Some(Color::BLACK),
|
||||
primary_working_color: Some(Color::BLACK),
|
||||
secondary_working_color: Some(Color::WHITE),
|
||||
fill_choice: Some(FillChoice::Solid(Color::BLACK)),
|
||||
enabled: Some(true),
|
||||
tracks_working_color: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolColorOptions {
|
||||
pub fn new_primary() -> Self {
|
||||
pub fn new_enabled() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn new_secondary() -> Self {
|
||||
pub fn new_disabled() -> Self {
|
||||
Self {
|
||||
color_type: ToolColorType::Secondary,
|
||||
..Default::default()
|
||||
fill_choice: Some(FillChoice::None),
|
||||
enabled: Some(false),
|
||||
tracks_working_color: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_none() -> Self {
|
||||
Self {
|
||||
color_type: ToolColorType::Custom,
|
||||
custom_color: None,
|
||||
..Default::default()
|
||||
}
|
||||
/// True when the slot is actively applied, i.e. `enabled` is `Some(true)`.
|
||||
/// `None` (mixed) and `Some(false)` both count as not actively applied.
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.enabled == Some(true)
|
||||
}
|
||||
|
||||
/// The active solid color in linear sRGB, suitable for storing in a working color or downstream rendering input.
|
||||
/// `fill_choice` is stored in gamma space (per [`FillChoice`]'s contract), so this method converts to linear before returning.
|
||||
pub fn active_color(&self) -> Option<Color> {
|
||||
match self.color_type {
|
||||
ToolColorType::Custom => self.custom_color,
|
||||
ToolColorType::Primary => self.primary_working_color,
|
||||
ToolColorType::Secondary => self.secondary_working_color,
|
||||
if !self.is_active() {
|
||||
return None;
|
||||
}
|
||||
Some(self.fill_choice.as_ref()?.as_solid()?.to_linear_srgb())
|
||||
}
|
||||
|
||||
pub fn apply_fill(&self, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
|
||||
if let Some(color) = self.active_color() {
|
||||
let fill = graphene_std::vector::style::Fill::solid(color.to_gamma_srgb());
|
||||
if !self.is_active() {
|
||||
return;
|
||||
}
|
||||
if let Some(FillChoice::Solid(color)) = &self.fill_choice {
|
||||
let fill = graphene_std::vector::style::Fill::Solid(*color);
|
||||
responses.add(GraphOperationMessage::FillSet { layer, fill });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_stroke(&self, weight: f64, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
|
||||
if let Some(color) = self.active_color() {
|
||||
let stroke = graphene_std::vector::style::Stroke::new(Some(color.to_gamma_srgb()), weight);
|
||||
if !self.is_active() {
|
||||
return;
|
||||
}
|
||||
if let Some(FillChoice::Solid(color)) = &self.fill_choice {
|
||||
let stroke = graphene_std::vector::style::Stroke::new(Some(*color), weight);
|
||||
responses.add(GraphOperationMessage::StrokeSet { layer, stroke });
|
||||
}
|
||||
}
|
||||
|
|
@ -76,49 +79,419 @@ impl ToolColorOptions {
|
|||
pub fn create_widgets(
|
||||
&self,
|
||||
label_text: impl Into<String>,
|
||||
color_allow_none: bool,
|
||||
reset_callback: impl Fn(&IconButton) -> Message + 'static + Send + Sync,
|
||||
radio_callback: fn(ToolColorType) -> WidgetCallback<()>,
|
||||
checkbox_callback: impl Fn(&CheckboxInput) -> Message + 'static + Send + Sync,
|
||||
color_callback: impl Fn(&ColorInput) -> Message + 'static + Send + Sync,
|
||||
) -> Vec<WidgetInstance> {
|
||||
let mut widgets = vec![TextLabel::new(label_text).widget_instance()];
|
||||
let checkbox_id = CheckboxId::new();
|
||||
// In the mixed state (`fill_choice` is `None`) the dash overlay covers the swatch, so the underlying widget value just drives the picker's initial position.
|
||||
// `FillChoice::None` gives it a neutral starting point.
|
||||
let mixed_color = self.fill_choice.is_none();
|
||||
let widget_value = self.fill_choice.clone().unwrap_or(FillChoice::None);
|
||||
let mixed_enabled = self.enabled.is_none();
|
||||
// In the mixed-enabled state the underlying `checked` value is hidden behind the indeterminate dash.
|
||||
// The frontend's click handler sends `true` when the user resolves the mixed state by clicking.
|
||||
let checked = self.enabled.unwrap_or(false);
|
||||
|
||||
if !color_allow_none {
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
} else {
|
||||
let reset = IconButton::new("CloseX", 12)
|
||||
.disabled(self.custom_color.is_none() && self.color_type == ToolColorType::Custom)
|
||||
.tooltip_label("Clear Color")
|
||||
.on_update(reset_callback);
|
||||
|
||||
widgets.push(Separator::new(SeparatorStyle::Related).widget_instance());
|
||||
widgets.push(reset.widget_instance());
|
||||
widgets.push(Separator::new(SeparatorStyle::Related).widget_instance());
|
||||
};
|
||||
|
||||
let entries = vec![
|
||||
("WorkingColorsPrimary", "Primary Working Color", ToolColorType::Primary),
|
||||
("WorkingColorsSecondary", "Secondary Working Color", ToolColorType::Secondary),
|
||||
("CustomColor", "Custom Color", ToolColorType::Custom),
|
||||
vec![
|
||||
CheckboxInput::new(checked).mixed(mixed_enabled).on_update(checkbox_callback).for_label(checkbox_id).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||
TextLabel::new(label_text).for_checkbox(checkbox_id).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||
ColorInput::new(widget_value)
|
||||
.mixed(mixed_color)
|
||||
.min_width(48)
|
||||
.max_width(48)
|
||||
.narrow(true)
|
||||
.on_update(color_callback)
|
||||
.widget_instance(),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(icon, label, color_type)| {
|
||||
let mut entry = RadioEntryData::new(format!("{color_type:?}")).tooltip_label(label).icon(icon);
|
||||
entry.on_update = radio_callback(color_type);
|
||||
entry
|
||||
})
|
||||
.collect();
|
||||
let radio = RadioInput::new(entries).selected_index(Some(self.color_type.clone() as u32)).widget_instance();
|
||||
widgets.push(radio);
|
||||
widgets.push(Separator::new(SeparatorStyle::Related).widget_instance());
|
||||
|
||||
let fill_choice = match self.active_color() {
|
||||
Some(color) => FillChoice::Solid(color.to_gamma_srgb()),
|
||||
None => FillChoice::None,
|
||||
};
|
||||
let color_button = ColorInput::new(fill_choice).allow_none(color_allow_none).on_update(color_callback);
|
||||
widgets.push(color_button.widget_instance());
|
||||
|
||||
widgets
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared per-tool state for drawing tools that produce a stroked-and-filled shape (Shape, Pen, Freehand, Spline).
|
||||
/// Bundles the weight, color, and selection-sync fields that would otherwise be duplicated across each tool's options struct.
|
||||
/// The displayed fill/stroke colors track the global working colors.
|
||||
pub struct DrawingToolState {
|
||||
/// The current stroke weight. `None` = mixed across selected layers.
|
||||
pub line_weight: Option<f64>,
|
||||
/// Persistent default weight, updated when the user edits the weight while no layer is selected.
|
||||
pub default_line_weight: f64,
|
||||
/// Set of layers we last synced from, used to detect real selection changes vs. internal node toggles.
|
||||
pub last_synced_selection: Vec<LayerNodeIdentifier>,
|
||||
/// The fill swatch's color, checkbox, and mixed state.
|
||||
pub fill: ToolColorOptions,
|
||||
/// The stroke swatch's color, checkbox, and mixed state.
|
||||
pub stroke: ToolColorOptions,
|
||||
/// When false (default), fill follows the secondary working color and stroke follows the primary; when true, the routing is reversed.
|
||||
/// Persisted per-tool. The Shape tool additionally persists it for each shape mode via its options.
|
||||
pub colors_swapped: bool,
|
||||
}
|
||||
|
||||
impl DrawingToolState {
|
||||
pub fn new(fill_enabled: bool) -> Self {
|
||||
Self {
|
||||
line_weight: Some(DEFAULT_STROKE_WIDTH),
|
||||
default_line_weight: DEFAULT_STROKE_WIDTH,
|
||||
last_synced_selection: Vec::new(),
|
||||
fill: if fill_enabled { ToolColorOptions::new_enabled() } else { ToolColorOptions::new_disabled() },
|
||||
stroke: ToolColorOptions::new_enabled(),
|
||||
colors_swapped: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The line weight to apply, falling back to the persistent default when [`Self::line_weight`] is `None` (mixed).
|
||||
pub fn effective_line_weight(&self) -> f64 {
|
||||
self.line_weight.unwrap_or(self.default_line_weight)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a `FillChoice::Solid` from a linear-space color, applying gamma conversion to display sRGB.
|
||||
/// Common helper used throughout the color-syncing code where working colors (linear) flow into swatches that store gamma-encoded colors.
|
||||
pub fn solid_gamma(color: Color) -> FillChoice {
|
||||
FillChoice::Solid(color.to_gamma_srgb())
|
||||
}
|
||||
|
||||
/// The fill working color (the source for the fill swatch when nothing is selected).
|
||||
/// Defaults to secondary, swapped to primary when the per-tool [`DrawingToolState::colors_swapped`] flag is set.
|
||||
pub fn fill_working_color(global: &DocumentToolData, colors_swapped: bool) -> Color {
|
||||
if colors_swapped { global.primary_color } else { global.secondary_color }
|
||||
}
|
||||
|
||||
/// The stroke working color (the source for the stroke swatch when nothing is selected).
|
||||
/// Defaults to primary, swapped to secondary when the per-tool [`DrawingToolState::colors_swapped`] flag is set.
|
||||
pub fn stroke_working_color(global: &DocumentToolData, colors_swapped: bool) -> Color {
|
||||
if colors_swapped { global.secondary_color } else { global.primary_color }
|
||||
}
|
||||
|
||||
/// Syncs fill and stroke from the selection (or working colors when empty). With `selection_changed = false`, preserves display values
|
||||
/// for inactive states instead of resetting them. Returns `true` if anything changed.
|
||||
pub fn sync_color_options(
|
||||
drawing: &mut DrawingToolState,
|
||||
natural_fill_enabled: bool,
|
||||
natural_stroke_enabled: bool,
|
||||
global: &DocumentToolData,
|
||||
document: &DocumentMessageHandler,
|
||||
selection_changed: bool,
|
||||
) -> bool {
|
||||
let fill_fallback = solid_gamma(fill_working_color(global, drawing.colors_swapped));
|
||||
let stroke_fallback = solid_gamma(stroke_working_color(global, drawing.colors_swapped));
|
||||
|
||||
let mut changed = false;
|
||||
|
||||
// FILL
|
||||
|
||||
let new_fill = if let Some(state) = graph_modification_utils::selected_fill_state(document) {
|
||||
let active = state.enabled == Some(true);
|
||||
let (display_choice, tracks_working) = match &state.fill_choice {
|
||||
Some(choice) if active => (Some(choice.clone()), false),
|
||||
Some(_) if selection_changed => (Some(fill_fallback.clone()), true),
|
||||
Some(_) => (drawing.fill.fill_choice.clone(), drawing.fill.tracks_working_color),
|
||||
None => (None, true),
|
||||
};
|
||||
(state.enabled, display_choice, tracks_working)
|
||||
} else {
|
||||
// On a real deselect, revert to the working color; otherwise preserve the displayed value.
|
||||
let display_choice = if selection_changed { Some(fill_fallback) } else { drawing.fill.fill_choice.clone() };
|
||||
let tracks_working = if selection_changed { true } else { drawing.fill.tracks_working_color };
|
||||
(Some(natural_fill_enabled), display_choice, tracks_working)
|
||||
};
|
||||
if drawing.fill.enabled != new_fill.0 || drawing.fill.fill_choice != new_fill.1 || drawing.fill.tracks_working_color != new_fill.2 {
|
||||
drawing.fill.enabled = new_fill.0;
|
||||
drawing.fill.fill_choice = new_fill.1;
|
||||
drawing.fill.tracks_working_color = new_fill.2;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// STROKE
|
||||
|
||||
let new_stroke = if let Some(state) = graph_modification_utils::selected_stroke_state(document) {
|
||||
let active = state.enabled == Some(true);
|
||||
let (display_choice, tracks_working) = match state.optional_color {
|
||||
Some(color) if active => (Some(color.map_or(FillChoice::None, FillChoice::Solid)), false),
|
||||
Some(_) if selection_changed => (Some(stroke_fallback.clone()), true),
|
||||
Some(_) => (drawing.stroke.fill_choice.clone(), drawing.stroke.tracks_working_color),
|
||||
None => (None, true),
|
||||
};
|
||||
(state.enabled, display_choice, tracks_working)
|
||||
} else {
|
||||
let display_choice = if selection_changed { Some(stroke_fallback) } else { drawing.stroke.fill_choice.clone() };
|
||||
let tracks_working = if selection_changed { true } else { drawing.stroke.tracks_working_color };
|
||||
(Some(natural_stroke_enabled), display_choice, tracks_working)
|
||||
};
|
||||
if drawing.stroke.enabled != new_stroke.0 || drawing.stroke.fill_choice != new_stroke.1 || drawing.stroke.tracks_working_color != new_stroke.2 {
|
||||
drawing.stroke.enabled = new_stroke.0;
|
||||
drawing.stroke.fill_choice = new_stroke.1;
|
||||
drawing.stroke.tracks_working_color = new_stroke.2;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
changed
|
||||
}
|
||||
|
||||
/// Full SelectionChanged update for a drawing tool: syncs fill/stroke colors and the stroke weight. Returns `true` if the layout needs refreshing.
|
||||
pub fn sync_drawing_state(drawing: &mut DrawingToolState, natural_fill_enabled: bool, natural_stroke_enabled: bool, global: &DocumentToolData, document: &DocumentMessageHandler) -> bool {
|
||||
let selection_changed = selection_changed_since_last_sync(&mut drawing.last_synced_selection, document);
|
||||
let mut needs_refresh = sync_color_options(drawing, natural_fill_enabled, natural_stroke_enabled, global, document, selection_changed);
|
||||
|
||||
let new_line_weight = match compute_weight_sync(document) {
|
||||
WeightSyncOutcome::Set(weight) => Some(weight),
|
||||
WeightSyncOutcome::Mixed => None,
|
||||
// On a real selection change, revert to the default; otherwise preserve.
|
||||
WeightSyncOutcome::NoStrokes | WeightSyncOutcome::NoSelection => {
|
||||
if selection_changed {
|
||||
Some(drawing.default_line_weight)
|
||||
} else {
|
||||
drawing.line_weight
|
||||
}
|
||||
}
|
||||
};
|
||||
if drawing.line_weight != new_line_weight {
|
||||
drawing.line_weight = new_line_weight;
|
||||
needs_refresh = true;
|
||||
}
|
||||
|
||||
needs_refresh
|
||||
}
|
||||
|
||||
/// Same as [`sync_color_options`] but for tools that only have a fill option (e.g., text). The fill follows the given working color when nothing is selected.
|
||||
pub fn sync_fill_only(fill: &mut ToolColorOptions, natural_fill_enabled: bool, fill_color: Color, document: &DocumentMessageHandler, selection_changed: bool) -> bool {
|
||||
let fill_fallback = solid_gamma(fill_color);
|
||||
|
||||
let new_fill = if let Some(state) = graph_modification_utils::selected_fill_state(document) {
|
||||
let active = state.enabled == Some(true);
|
||||
let (display_choice, tracks_working_color) = match &state.fill_choice {
|
||||
Some(choice) if active => (Some(choice.clone()), false),
|
||||
Some(_) if selection_changed => (Some(fill_fallback.clone()), true),
|
||||
Some(_) => (fill.fill_choice.clone(), fill.tracks_working_color),
|
||||
None => (None, true),
|
||||
};
|
||||
(state.enabled, display_choice, tracks_working_color)
|
||||
} else {
|
||||
let display_choice = if selection_changed { Some(fill_fallback) } else { fill.fill_choice.clone() };
|
||||
let tracks_working = if selection_changed { true } else { fill.tracks_working_color };
|
||||
(Some(natural_fill_enabled), display_choice, tracks_working)
|
||||
};
|
||||
|
||||
if fill.enabled != new_fill.0 || fill.fill_choice != new_fill.1 || fill.tracks_working_color != new_fill.2 {
|
||||
fill.enabled = new_fill.0;
|
||||
fill.fill_choice = new_fill.1;
|
||||
fill.tracks_working_color = new_fill.2;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn apply_fill_color_pick(drawing: &mut DrawingToolState, fill_choice: FillChoice, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
apply_fill_only_color_pick(&mut drawing.fill, fill_choice, drawing.colors_swapped, document, responses);
|
||||
}
|
||||
|
||||
/// Single-slot variant of [`apply_fill_color_pick`] (e.g. for text). `slot_is_primary` says which working color this slot binds to.
|
||||
pub fn apply_fill_only_color_pick(fill: &mut ToolColorOptions, fill_choice: FillChoice, slot_is_primary: bool, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
fill.fill_choice = Some(fill_choice.clone());
|
||||
fill.enabled = Some(true);
|
||||
fill.tracks_working_color = false;
|
||||
if has_selection(document) {
|
||||
if document.network_interface.transaction_status() == TransactionStatus::Finished {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
}
|
||||
graph_modification_utils::set_fill_for_selected_layers(fill_choice, document, responses);
|
||||
} else if let FillChoice::Solid(color) = fill_choice {
|
||||
// Swatch is gamma; working colors are linear.
|
||||
responses.add(ToolMessage::SelectWorkingColor {
|
||||
color: color.to_linear_srgb(),
|
||||
primary: slot_is_primary,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies a user-picked stroke color. With a selection, writes to the layers; with none, pushes to the swap-routed working color slot.
|
||||
pub fn apply_stroke_color_pick(drawing: &mut DrawingToolState, color: Option<Color>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
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 document.network_interface.transaction_status() == TransactionStatus::Finished {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
}
|
||||
graph_modification_utils::set_stroke_color_for_selected_layers(color, drawing.effective_line_weight(), document, responses);
|
||||
} else if let Some(color) = color {
|
||||
// Swatch is gamma; working colors are linear.
|
||||
responses.add(ToolMessage::SelectWorkingColor {
|
||||
color: color.to_linear_srgb(),
|
||||
primary: !drawing.colors_swapped,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggles the fill checkbox: re-applies the preserved color when enabled, removes the fill node when disabled.
|
||||
pub fn apply_fill_enabled(drawing: &mut DrawingToolState, enabled: bool, global: &DocumentToolData, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
apply_fill_only_enabled(&mut drawing.fill, enabled, fill_working_color(global, drawing.colors_swapped), document, responses);
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
}
|
||||
if enabled {
|
||||
// Mixed re-tick has no per-layer color to restore; fall back to the working color and keep tracking it.
|
||||
let fill_choice = fill.fill_choice.clone().unwrap_or_else(|| {
|
||||
fill.tracks_working_color = true;
|
||||
solid_gamma(working_color)
|
||||
});
|
||||
fill.fill_choice = Some(fill_choice.clone());
|
||||
graph_modification_utils::set_fill_for_selected_layers(fill_choice, document, responses);
|
||||
} else {
|
||||
// Unticking from mixed: capture the working color as the saved value so the swatch keeps following the link.
|
||||
if fill.fill_choice.is_none() {
|
||||
fill.fill_choice = Some(solid_gamma(working_color));
|
||||
fill.tracks_working_color = true;
|
||||
}
|
||||
graph_modification_utils::remove_fill_for_selected_layers(document, responses);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
}
|
||||
if enabled {
|
||||
let stroke_choice = drawing.stroke.fill_choice.clone().unwrap_or_else(|| {
|
||||
drawing.stroke.tracks_working_color = true;
|
||||
solid_gamma(stroke_working_color(global, drawing.colors_swapped))
|
||||
});
|
||||
drawing.stroke.fill_choice = Some(stroke_choice.clone());
|
||||
graph_modification_utils::set_stroke_color_for_selected_layers(stroke_choice.as_solid(), drawing.effective_line_weight(), document, responses);
|
||||
} else {
|
||||
if drawing.stroke.fill_choice.is_none() {
|
||||
drawing.stroke.fill_choice = Some(solid_gamma(stroke_working_color(global, drawing.colors_swapped)));
|
||||
drawing.stroke.tracks_working_color = true;
|
||||
}
|
||||
graph_modification_utils::remove_stroke_for_selected_layers(document, responses);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
drawing.default_line_weight = line_weight;
|
||||
}
|
||||
graph_modification_utils::set_stroke_weight_for_selected_layers(line_weight, document, responses);
|
||||
}
|
||||
|
||||
/// Propagates working colors to the tool's swatches. With no selection both slots refresh; with a selection, only slots tracking the working color.
|
||||
pub fn apply_working_colors(drawing: &mut DrawingToolState, global: &DocumentToolData, document: &DocumentMessageHandler) {
|
||||
refresh_slot_working_color(&mut drawing.fill, fill_working_color(global, drawing.colors_swapped), document);
|
||||
refresh_slot_working_color(&mut drawing.stroke, stroke_working_color(global, drawing.colors_swapped), document);
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
slot.fill_choice = Some(solid_gamma(working_color));
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the tool's swatches to the working colors. Called on tool deactivation and shape-mode changes.
|
||||
pub fn reset_colors_on_deactivation(drawing: &mut DrawingToolState, global: &DocumentToolData) {
|
||||
drawing.fill.fill_choice = Some(solid_gamma(fill_working_color(global, drawing.colors_swapped)));
|
||||
drawing.stroke.fill_choice = Some(solid_gamma(stroke_working_color(global, drawing.colors_swapped)));
|
||||
drawing.fill.tracks_working_color = true;
|
||||
drawing.stroke.tracks_working_color = true;
|
||||
}
|
||||
|
||||
/// Handles the "Swap Fill/Stroke" button. Stroke can only hold a solid color, so a gradient fill collapses to `None` when moved.
|
||||
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) {
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
}
|
||||
|
||||
// The new fill takes the old stroke's value as-is; the new stroke takes the old fill (with any gradient collapsed to `None`,
|
||||
// since stroke can only hold a solid color). `None` (mixed) on either side propagates as `None` to the other.
|
||||
let new_fill = drawing.stroke.fill_choice.clone();
|
||||
let new_stroke = drawing.fill.fill_choice.as_ref().map(|c| c.as_solid().map_or(FillChoice::None, FillChoice::Solid));
|
||||
let (new_fill_tracks, new_stroke_tracks) = (drawing.stroke.tracks_working_color, drawing.fill.tracks_working_color);
|
||||
|
||||
drawing.fill.fill_choice = new_fill.clone();
|
||||
drawing.stroke.fill_choice = new_stroke.clone();
|
||||
drawing.fill.tracks_working_color = new_fill_tracks;
|
||||
drawing.stroke.tracks_working_color = new_stroke_tracks;
|
||||
|
||||
if has_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
|
||||
{
|
||||
graph_modification_utils::set_fill_for_selected_layers(choice, document, responses);
|
||||
}
|
||||
if drawing.stroke.is_active()
|
||||
&& let Some(choice) = new_stroke
|
||||
{
|
||||
graph_modification_utils::set_stroke_color_for_selected_layers(choice.as_solid(), drawing.effective_line_weight(), document, responses);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the cache and returns `true` if the current selection differs from the last-synced one. Cache stays sorted to keep comparisons cheap.
|
||||
pub fn selection_changed_since_last_sync(last_synced: &mut Vec<LayerNodeIdentifier>, document: &DocumentMessageHandler) -> bool {
|
||||
let mut current: Vec<LayerNodeIdentifier> = document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface).collect();
|
||||
|
||||
current.sort();
|
||||
|
||||
let changed = current != *last_synced;
|
||||
*last_synced = current;
|
||||
changed
|
||||
}
|
||||
|
||||
/// How the weight widget should update from inspecting selected layers' strokes.
|
||||
pub enum WeightSyncOutcome {
|
||||
/// All strokes share this weight.
|
||||
Set(f64),
|
||||
/// Stroke weights differ (or some layers lack a stroke): show the mixed dash.
|
||||
Mixed,
|
||||
/// Selection has no strokes: reset to the tool's default on a real selection change, otherwise preserve.
|
||||
NoStrokes,
|
||||
/// No selection: preserve the current value.
|
||||
NoSelection,
|
||||
}
|
||||
|
||||
/// 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();
|
||||
|
||||
if layers.is_empty() {
|
||||
return WeightSyncOutcome::NoSelection;
|
||||
}
|
||||
|
||||
let stroke_weights: Vec<f64> = layers.iter().filter_map(|l| graph_modification_utils::get_stroke_width(*l, &document.network_interface)).collect();
|
||||
|
||||
if stroke_weights.is_empty() {
|
||||
return WeightSyncOutcome::NoStrokes;
|
||||
}
|
||||
|
||||
if stroke_weights.len() != layers.len() {
|
||||
return WeightSyncOutcome::Mixed;
|
||||
}
|
||||
|
||||
let first = stroke_weights[0];
|
||||
let all_same = stroke_weights.iter().all(|&w| (w - first).abs() < f64::EPSILON * 100.);
|
||||
if all_same { WeightSyncOutcome::Set(first) } else { WeightSyncOutcome::Mixed }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use graphene_std::raster_types::{CPU, GPU, Image, Raster};
|
|||
use graphene_std::subpath::Subpath;
|
||||
use graphene_std::text::{Font, TypesettingConfig};
|
||||
use graphene_std::vector::misc::ManipulatorPointId;
|
||||
use graphene_std::vector::style::{Fill, Gradient};
|
||||
use graphene_std::vector::style::{Fill, FillChoice, Gradient};
|
||||
use graphene_std::vector::{GradientStops, PointId, SegmentId, VectorModificationType};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
|
|
@ -526,6 +526,176 @@ pub fn set_stroke_weight_for_selected_layers(weight: f64, document: &DocumentMes
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns the `Fill` value from a layer's upstream Fill node.
|
||||
pub fn get_fill_value(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Fill> {
|
||||
let fill_index = graphene_std::vector::fill::FillInput::<Fill>::INDEX;
|
||||
let tagged = NodeGraphLayer::new(layer, network_interface).find_input(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER), fill_index)?;
|
||||
if let TaggedValue::Fill(fill) = tagged { Some(fill.clone()) } else { None }
|
||||
}
|
||||
|
||||
/// Returns the stroke color from a layer's upstream Stroke node.
|
||||
pub fn get_stroke_color(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Option<Color>> {
|
||||
let color_index = graphene_std::vector::stroke::ColorInput::INDEX;
|
||||
let tagged = NodeGraphLayer::new(layer, network_interface).find_input(&DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER), color_index)?;
|
||||
if let TaggedValue::Color(color) = tagged { Some(*color) } else { None }
|
||||
}
|
||||
|
||||
/// Aggregated fill state across all selected non-artboard layers.
|
||||
pub struct SelectedFillState {
|
||||
/// `None` means mixed values between selected layers.
|
||||
pub enabled: Option<bool>,
|
||||
/// `None` means mixed values between selected layers.
|
||||
pub fill_choice: Option<FillChoice>,
|
||||
}
|
||||
|
||||
/// Aggregated stroke state across all selected non-artboard layers.
|
||||
pub struct SelectedStrokeState {
|
||||
/// `None` means mixed values between selected layers.
|
||||
pub enabled: Option<bool>,
|
||||
/// `None` means mixed values between selected layers.
|
||||
pub optional_color: Option<Option<Color>>,
|
||||
}
|
||||
|
||||
/// 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 [`FillChoice::None`].
|
||||
/// Unticked means there is no Fill node. Returns `None` only when no 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| {
|
||||
if get_fill_id(layer, &document.network_interface).is_none() {
|
||||
return (false, FillChoice::None);
|
||||
}
|
||||
let fill_choice = get_fill_value(layer, &document.network_interface).map_or(FillChoice::None, FillChoice::from);
|
||||
(true, fill_choice)
|
||||
});
|
||||
|
||||
let (initial_enabled, initial_choice) = per_layer.next()?;
|
||||
let mut enabled_mixed = false;
|
||||
let mut color_mixed = false;
|
||||
let mut comparison_enabled = initial_enabled;
|
||||
let mut comparison_choice = initial_choice;
|
||||
for (enabled, fill_choice) in per_layer {
|
||||
if enabled != initial_enabled {
|
||||
enabled_mixed = true;
|
||||
}
|
||||
if enabled {
|
||||
if comparison_enabled {
|
||||
if fill_choice != comparison_choice {
|
||||
color_mixed = true;
|
||||
}
|
||||
} else {
|
||||
comparison_enabled = true;
|
||||
comparison_choice = fill_choice;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(SelectedFillState {
|
||||
enabled: (!enabled_mixed).then_some(initial_enabled),
|
||||
fill_choice: (!color_mixed).then_some(comparison_choice),
|
||||
})
|
||||
}
|
||||
|
||||
/// Reads the stroke 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 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| {
|
||||
if get_stroke_id(layer, &document.network_interface).is_none() {
|
||||
return (false, None);
|
||||
}
|
||||
let color = get_stroke_color(layer, &document.network_interface).flatten();
|
||||
(true, color)
|
||||
});
|
||||
|
||||
let (initial_enabled, initial_color) = per_layer.next()?;
|
||||
let mut enabled_mixed = false;
|
||||
let mut color_mixed = false;
|
||||
let mut comparison_enabled = initial_enabled;
|
||||
let mut comparison_color = initial_color;
|
||||
for (enabled, color) in per_layer {
|
||||
if enabled != initial_enabled {
|
||||
enabled_mixed = true;
|
||||
}
|
||||
if enabled {
|
||||
if comparison_enabled {
|
||||
if color != comparison_color {
|
||||
color_mixed = true;
|
||||
}
|
||||
} else {
|
||||
comparison_enabled = true;
|
||||
comparison_color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(SelectedStrokeState {
|
||||
enabled: (!enabled_mixed).then_some(initial_enabled),
|
||||
optional_color: (!color_mixed).then_some(comparison_color),
|
||||
})
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
let existing_gradient = get_fill_value(layer, &document.network_interface).and_then(|f| match f {
|
||||
Fill::Gradient(g) => Some(g),
|
||||
_ => None,
|
||||
});
|
||||
let fill = fill_choice.clone().to_fill(existing_gradient.as_ref());
|
||||
responses.add(GraphOperationMessage::FillSet { layer, fill });
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the stroke color on all selected non-artboard layers. Layers without an existing Stroke node get one created using
|
||||
/// 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 {
|
||||
if let Some(node_id) = get_stroke_id(layer, &document.network_interface) {
|
||||
let input_index = graphene_std::vector::stroke::ColorInput::INDEX;
|
||||
let value = TaggedValue::Color(color);
|
||||
responses.add(NodeGraphMessage::SetInputValue { node_id, input_index, value });
|
||||
} else {
|
||||
let stroke = graphene_std::vector::style::Stroke::new(color, weight);
|
||||
responses.add(GraphOperationMessage::StrokeSet { layer, stroke });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
if let Some(node_id) = get_fill_id(layer, &document.network_interface) {
|
||||
responses.add(NodeGraphMessage::DeleteNodes {
|
||||
node_ids: vec![node_id],
|
||||
delete_children: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
if let Some(node_id) = get_stroke_id(layer, &document.network_interface) {
|
||||
responses.add(NodeGraphMessage::DeleteNodes {
|
||||
node_ids: vec![node_id],
|
||||
delete_children: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
}
|
||||
|
||||
/// Reads a specific input from the matching proto node on the first selected non-artboard layer that has one.
|
||||
/// Used by tool control bars to mirror per-shape parameters (sides, arc type, turns, etc.) from the selection
|
||||
/// into the control bar's input widget state without each call site re-implementing the layer iteration.
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ use std::collections::VecDeque;
|
|||
use std::f64::consts::{PI, TAU};
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ShapeType {
|
||||
#[default]
|
||||
Polygon = 0,
|
||||
|
|
@ -41,6 +41,26 @@ pub enum ShapeType {
|
|||
}
|
||||
|
||||
impl ShapeType {
|
||||
/// Every shape mode, in dropdown order. Used to seed per-mode default maps.
|
||||
pub const ALL: &[ShapeType] = &[
|
||||
ShapeType::Polygon,
|
||||
ShapeType::Star,
|
||||
ShapeType::Circle,
|
||||
ShapeType::Arc,
|
||||
ShapeType::Spiral,
|
||||
ShapeType::Grid,
|
||||
ShapeType::Arrow,
|
||||
ShapeType::Line, // KEEP THIS AT THE END
|
||||
ShapeType::Rectangle, // KEEP THIS AT THE END
|
||||
ShapeType::Ellipse, // KEEP THIS AT THE END
|
||||
];
|
||||
|
||||
/// True if this shape mode's fill checkbox is ticked by default when nothing is selected.
|
||||
/// Spiral/Grid/Line are open paths and default to fill-off, the closed shapes default to fill-on.
|
||||
pub fn defaults_to_fill(&self) -> bool {
|
||||
matches!(self, Self::Polygon | Self::Star | Self::Circle | Self::Arc | Self::Rectangle | Self::Ellipse | Self::Arrow)
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
(match self {
|
||||
Self::Polygon => "Polygon",
|
||||
|
|
@ -50,9 +70,9 @@ impl ShapeType {
|
|||
Self::Spiral => "Spiral",
|
||||
Self::Grid => "Grid",
|
||||
Self::Arrow => "Arrow",
|
||||
Self::Line => "Line",
|
||||
Self::Rectangle => "Rectangle",
|
||||
Self::Ellipse => "Ellipse",
|
||||
Self::Line => "Line", // KEEP THIS AT THE END
|
||||
Self::Rectangle => "Rectangle", // KEEP THIS AT THE END
|
||||
Self::Ellipse => "Ellipse", // KEEP THIS AT THE END
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@ use crate::messages::portfolio::document::graph_operation::transform_utils::get_
|
|||
use crate::messages::portfolio::document::node_graph::document_node_definitions::{DefinitionIdentifier, resolve_proto_node_type};
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::FlowType;
|
||||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
|
||||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, solid_gamma};
|
||||
use graph_craft::document::NodeId;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graphene_std::Color;
|
||||
use graphene_std::brush::brush_stroke::{BrushInputSample, BrushStroke, BrushStyle};
|
||||
use graphene_std::raster::BlendMode;
|
||||
use graphene_std::vector::style::FillChoice;
|
||||
|
||||
const BRUSH_MAX_SIZE: f64 = 5000.;
|
||||
|
||||
|
|
@ -73,13 +74,12 @@ pub enum BrushToolMessageOptionsUpdate {
|
|||
BlendMode(BlendMode),
|
||||
ChangeDiameter(f64),
|
||||
Color(Option<Color>),
|
||||
ColorType(ToolColorType),
|
||||
Diameter(f64),
|
||||
DrawMode(DrawMode),
|
||||
Flow(f64),
|
||||
Hardness(f64),
|
||||
Spacing(f64),
|
||||
WorkingColors(Option<Color>, Option<Color>),
|
||||
WorkingColorsChanged,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
|
|
@ -104,6 +104,18 @@ impl ToolMetadata for BrushTool {
|
|||
impl LayoutHolder for BrushTool {
|
||||
fn layout(&self) -> Layout {
|
||||
let mut widgets = vec![
|
||||
ColorInput::new(self.options.color.fill_choice.clone().unwrap_or(FillChoice::None))
|
||||
.mixed(self.options.color.fill_choice.is_none())
|
||||
.narrow(true)
|
||||
.on_update(|color: &ColorInput| {
|
||||
BrushToolMessage::UpdateOptions {
|
||||
// The picker emits gamma-space colors; working colors are stored in linear sRGB.
|
||||
options: BrushToolMessageOptionsUpdate::Color(color.value.as_solid().map(|c| c.to_linear_srgb())),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
NumberInput::new(Some(self.options.diameter))
|
||||
.label("Diameter")
|
||||
.min(1.)
|
||||
|
|
@ -170,33 +182,6 @@ impl LayoutHolder for BrushTool {
|
|||
.collect();
|
||||
widgets.push(RadioInput::new(draw_mode_entries).selected_index(Some(self.options.draw_mode as u32)).widget_instance());
|
||||
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
|
||||
widgets.append(&mut self.options.color.create_widgets(
|
||||
"Color",
|
||||
false,
|
||||
|_| {
|
||||
BrushToolMessage::UpdateOptions {
|
||||
options: BrushToolMessageOptionsUpdate::Color(None),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
|color_type: ToolColorType| {
|
||||
WidgetCallback::new(move |_| {
|
||||
BrushToolMessage::UpdateOptions {
|
||||
options: BrushToolMessageOptionsUpdate::ColorType(color_type.clone()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
},
|
||||
|color: &ColorInput| {
|
||||
BrushToolMessage::UpdateOptions {
|
||||
options: BrushToolMessageOptionsUpdate::Color(color.value.as_solid().map(|color| color.to_linear_srgb())),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
));
|
||||
|
||||
widgets.push(Separator::new(SeparatorStyle::Related).widget_instance());
|
||||
|
||||
let blend_mode_entries: Vec<Vec<_>> = BlendMode::list()
|
||||
|
|
@ -254,13 +239,13 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Brus
|
|||
BrushToolMessageOptionsUpdate::Flow(flow) => self.options.flow = flow,
|
||||
BrushToolMessageOptionsUpdate::Spacing(spacing) => self.options.spacing = spacing,
|
||||
BrushToolMessageOptionsUpdate::Color(color) => {
|
||||
self.options.color.custom_color = color;
|
||||
self.options.color.color_type = ToolColorType::Custom;
|
||||
// User picked a color: push to the global primary working color (no tool-local customization).
|
||||
if let Some(color) = color {
|
||||
responses.add(ToolMessage::SelectWorkingColor { color, primary: true });
|
||||
}
|
||||
}
|
||||
BrushToolMessageOptionsUpdate::ColorType(color_type) => self.options.color.color_type = color_type,
|
||||
BrushToolMessageOptionsUpdate::WorkingColors(primary, secondary) => {
|
||||
self.options.color.primary_working_color = primary;
|
||||
self.options.color.secondary_working_color = secondary;
|
||||
BrushToolMessageOptionsUpdate::WorkingColorsChanged => {
|
||||
self.options.color.fill_choice = Some(solid_gamma(context.global_tool_data.primary_color));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -355,9 +340,7 @@ impl Fsm for BrushToolFsmState {
|
|||
tool_options: &Self::ToolOptions,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
let ToolActionMessageContext {
|
||||
document, global_tool_data, input, ..
|
||||
} = tool_action_data;
|
||||
let ToolActionMessageContext { document, input, .. } = tool_action_data;
|
||||
|
||||
let ToolMessage::Brush(event) = event else { return self };
|
||||
match (self, event) {
|
||||
|
|
@ -450,7 +433,7 @@ impl Fsm for BrushToolFsmState {
|
|||
}
|
||||
(_, BrushToolMessage::WorkingColorChanged) => {
|
||||
responses.add(BrushToolMessage::UpdateOptions {
|
||||
options: BrushToolMessageOptionsUpdate::WorkingColors(Some(global_tool_data.primary_color), Some(global_tool_data.secondary_color)),
|
||||
options: BrushToolMessageOptionsUpdate::WorkingColorsChanged,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
use super::tool_prelude::*;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::tool::common_functionality::color_selector::solid_gamma;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
|
||||
use graphene_std::raster::color::Color;
|
||||
use graphene_std::vector::style::Fill;
|
||||
|
||||
#[derive(Default, ExtractField)]
|
||||
pub struct FillTool {
|
||||
fsm_state: FillToolFsmState,
|
||||
primary_color: Color,
|
||||
}
|
||||
|
||||
#[impl_message(Message, ToolMessage, Fill)]
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(PartialEq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum FillToolMessage {
|
||||
// Standard messages
|
||||
Abort,
|
||||
|
|
@ -22,6 +25,7 @@ pub enum FillToolMessage {
|
|||
PointerUp,
|
||||
FillPrimaryColor,
|
||||
FillSecondaryColor,
|
||||
SetColor { color: Option<Color> },
|
||||
}
|
||||
|
||||
impl ToolMetadata for FillTool {
|
||||
|
|
@ -38,13 +42,39 @@ impl ToolMetadata for FillTool {
|
|||
|
||||
impl LayoutHolder for FillTool {
|
||||
fn layout(&self) -> Layout {
|
||||
Layout::default()
|
||||
let widgets = vec![
|
||||
ColorInput::new(solid_gamma(self.primary_color))
|
||||
.narrow(true)
|
||||
.on_update(|color: &ColorInput| {
|
||||
FillToolMessage::SetColor {
|
||||
color: color.value.as_solid().map(|c| c.to_linear_srgb()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_instance(),
|
||||
];
|
||||
Layout(vec![LayoutGroup::row(widgets)])
|
||||
}
|
||||
}
|
||||
|
||||
#[message_handler_data]
|
||||
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for FillTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
|
||||
// User picked a color in the control bar: push it to the global primary working color (no tool-local customization)
|
||||
if let ToolMessage::Fill(FillToolMessage::SetColor { color: Some(color) }) = &message {
|
||||
responses.add(ToolMessage::SelectWorkingColor { color: *color, primary: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// Mirror the global primary working color into the control bar's color swatch
|
||||
if matches!(message, ToolMessage::Fill(FillToolMessage::WorkingColorChanged)) {
|
||||
let new_color = context.global_tool_data.primary_color;
|
||||
if self.primary_color != new_color {
|
||||
self.primary_color = new_color;
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
}
|
||||
}
|
||||
|
||||
self.fsm_state.process_event(message, &mut (), context, &(), responses, true);
|
||||
}
|
||||
fn actions(&self) -> ActionList {
|
||||
|
|
@ -105,7 +135,7 @@ impl Fsm for FillToolFsmState {
|
|||
let ToolMessage::Fill(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(_, FillToolMessage::Overlays { context: mut overlay_context }) => {
|
||||
// Choose the working color to preview
|
||||
// Choose the color to preview
|
||||
let use_secondary = input.keyboard.get(Key::Shift as usize);
|
||||
let preview_color = if use_secondary { global_tool_data.secondary_color } else { global_tool_data.primary_color };
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
use super::tool_prelude::*;
|
||||
use crate::consts::DEFAULT_STROKE_WIDTH;
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_network_node_type;
|
||||
use crate::messages::portfolio::document::overlays::utility_functions::path_endpoint_overlays;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
|
||||
use crate::messages::tool::common_functionality::color_selector::{
|
||||
DrawingToolState, apply_fill_color_pick, apply_fill_enabled, apply_line_weight, apply_stroke_color_pick, apply_stroke_enabled, apply_working_colors, reset_colors_on_deactivation,
|
||||
swap_fill_and_stroke, sync_drawing_state,
|
||||
};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::common_functionality::utility_functions::should_extend;
|
||||
use glam::DVec2;
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::Color;
|
||||
use graphene_std::vector::VectorModificationType;
|
||||
use graphene_std::vector::style::FillChoice;
|
||||
use graphene_std::vector::{PointId, SegmentId};
|
||||
|
||||
#[derive(Default, ExtractField)]
|
||||
|
|
@ -22,17 +25,13 @@ pub struct FreehandTool {
|
|||
}
|
||||
|
||||
pub struct FreehandOptions {
|
||||
line_weight: f64,
|
||||
fill: ToolColorOptions,
|
||||
stroke: ToolColorOptions,
|
||||
drawing: DrawingToolState,
|
||||
}
|
||||
|
||||
impl Default for FreehandOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
line_weight: DEFAULT_STROKE_WIDTH,
|
||||
fill: ToolColorOptions::new_none(),
|
||||
stroke: ToolColorOptions::new_primary(),
|
||||
drawing: DrawingToolState::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -57,12 +56,13 @@ pub enum FreehandToolMessage {
|
|||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum FreehandOptionsUpdate {
|
||||
FillColor(Option<Color>),
|
||||
FillColorType(ToolColorType),
|
||||
FillColor(FillChoice),
|
||||
FillEnabled(bool),
|
||||
LineWeight(f64),
|
||||
StrokeColor(Option<Color>),
|
||||
StrokeColorType(ToolColorType),
|
||||
WorkingColors(Option<Color>, Option<Color>),
|
||||
StrokeEnabled(bool),
|
||||
SwapFillAndStroke,
|
||||
WorkingColorsChanged,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
|
|
@ -84,76 +84,79 @@ impl ToolMetadata for FreehandTool {
|
|||
}
|
||||
}
|
||||
|
||||
fn create_weight_widget(line_weight: f64) -> WidgetInstance {
|
||||
NumberInput::new(Some(line_weight))
|
||||
fn create_weight_widget(line_weight: Option<f64>, disabled: bool) -> WidgetInstance {
|
||||
NumberInput::new(line_weight)
|
||||
.unit(" px")
|
||||
.label("Weight")
|
||||
.min(1.)
|
||||
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
.min_width(100)
|
||||
.narrow(true)
|
||||
.disabled(disabled)
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
FreehandToolMessage::UpdateOptions {
|
||||
options: FreehandOptionsUpdate::LineWeight(number_input.value.unwrap()),
|
||||
if let Some(value) = number_input.value {
|
||||
FreehandToolMessage::UpdateOptions {
|
||||
options: FreehandOptionsUpdate::LineWeight(value),
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
Message::NoOp
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.on_commit(|_| DocumentMessage::StartTransaction.into())
|
||||
.widget_instance()
|
||||
}
|
||||
|
||||
impl LayoutHolder for FreehandTool {
|
||||
fn layout(&self) -> Layout {
|
||||
let mut widgets = self.options.fill.create_widgets(
|
||||
"Fill",
|
||||
true,
|
||||
|_| {
|
||||
let mut widgets = self.options.drawing.fill.create_widgets(
|
||||
"Fill:",
|
||||
|checkbox: &CheckboxInput| {
|
||||
FreehandToolMessage::UpdateOptions {
|
||||
options: FreehandOptionsUpdate::FillColor(None),
|
||||
options: FreehandOptionsUpdate::FillEnabled(checkbox.checked),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
|color_type: ToolColorType| {
|
||||
WidgetCallback::new(move |_| {
|
||||
FreehandToolMessage::UpdateOptions {
|
||||
options: FreehandOptionsUpdate::FillColorType(color_type.clone()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
},
|
||||
|color: &ColorInput| {
|
||||
FreehandToolMessage::UpdateOptions {
|
||||
options: FreehandOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
|
||||
options: FreehandOptionsUpdate::FillColor(color.value.clone()),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
);
|
||||
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
|
||||
widgets.append(&mut self.options.stroke.create_widgets(
|
||||
"Stroke",
|
||||
true,
|
||||
|_| {
|
||||
FreehandToolMessage::UpdateOptions {
|
||||
options: FreehandOptionsUpdate::StrokeColor(None),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
|color_type: ToolColorType| {
|
||||
WidgetCallback::new(move |_| {
|
||||
widgets.push(
|
||||
IconButton::new("SwapHorizontal", 16)
|
||||
.tooltip_label("Swap Fill/Stroke Colors")
|
||||
.on_update(|_| {
|
||||
FreehandToolMessage::UpdateOptions {
|
||||
options: FreehandOptionsUpdate::StrokeColorType(color_type.clone()),
|
||||
options: FreehandOptionsUpdate::SwapFillAndStroke,
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_instance(),
|
||||
);
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
|
||||
widgets.append(&mut self.options.drawing.stroke.create_widgets(
|
||||
"Stroke:",
|
||||
|checkbox: &CheckboxInput| {
|
||||
FreehandToolMessage::UpdateOptions {
|
||||
options: FreehandOptionsUpdate::StrokeEnabled(checkbox.checked),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
|color: &ColorInput| {
|
||||
FreehandToolMessage::UpdateOptions {
|
||||
options: FreehandOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
|
||||
options: FreehandOptionsUpdate::StrokeColor(color.value.as_solid()),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
));
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
widgets.push(create_weight_widget(self.options.line_weight));
|
||||
widgets.push(Separator::new(SeparatorStyle::Related).widget_instance());
|
||||
let weight_disabled = self.options.drawing.stroke.enabled == Some(false);
|
||||
widgets.push(create_weight_widget(self.options.drawing.line_weight, weight_disabled));
|
||||
|
||||
Layout(vec![LayoutGroup::row(widgets)])
|
||||
}
|
||||
|
|
@ -162,12 +165,18 @@ impl LayoutHolder for FreehandTool {
|
|||
#[message_handler_data]
|
||||
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for FreehandTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
|
||||
// On tool deactivation (Abort fires from the dispatcher's tool transition), reset the displayed fill/stroke colors so
|
||||
// the next activation starts fresh from the current working colors. The global swap state persists across tool switches.
|
||||
// Guarded on `Ready` so Esc-mid-drawing (which also fires Abort) doesn't wipe the user's customized fill/stroke options.
|
||||
if matches!(&message, ToolMessage::Freehand(FreehandToolMessage::Abort)) && self.fsm_state == FreehandToolFsmState::Ready {
|
||||
reset_colors_on_deactivation(&mut self.options.drawing, context.global_tool_data);
|
||||
}
|
||||
|
||||
if matches!(&message, ToolMessage::Freehand(FreehandToolMessage::SelectionChanged)) {
|
||||
if self.fsm_state == FreehandToolFsmState::Ready
|
||||
&& let Some(weight) = graph_modification_utils::first_selected_stroke_weight(context.document)
|
||||
&& self.options.line_weight != weight
|
||||
{
|
||||
self.options.line_weight = weight;
|
||||
if self.fsm_state != FreehandToolFsmState::Ready {
|
||||
return;
|
||||
}
|
||||
if sync_drawing_state(&mut self.options.drawing, false, true, context.global_tool_data, context.document) {
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
}
|
||||
return;
|
||||
|
|
@ -178,25 +187,26 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Free
|
|||
return;
|
||||
};
|
||||
match options {
|
||||
FreehandOptionsUpdate::FillColor(color) => {
|
||||
self.options.fill.custom_color = color;
|
||||
self.options.fill.color_type = ToolColorType::Custom;
|
||||
FreehandOptionsUpdate::FillColor(fill_choice) => {
|
||||
apply_fill_color_pick(&mut self.options.drawing, fill_choice, context.document, responses);
|
||||
}
|
||||
FreehandOptionsUpdate::FillEnabled(enabled) => {
|
||||
apply_fill_enabled(&mut self.options.drawing, enabled, context.global_tool_data, context.document, responses);
|
||||
}
|
||||
FreehandOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type,
|
||||
FreehandOptionsUpdate::LineWeight(line_weight) => {
|
||||
self.options.line_weight = line_weight;
|
||||
graph_modification_utils::set_stroke_weight_for_selected_layers(line_weight, context.document, responses);
|
||||
apply_line_weight(&mut self.options.drawing, line_weight, context.document, responses);
|
||||
}
|
||||
FreehandOptionsUpdate::StrokeColor(color) => {
|
||||
self.options.stroke.custom_color = color;
|
||||
self.options.stroke.color_type = ToolColorType::Custom;
|
||||
apply_stroke_color_pick(&mut self.options.drawing, color, context.document, responses);
|
||||
}
|
||||
FreehandOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type,
|
||||
FreehandOptionsUpdate::WorkingColors(primary, secondary) => {
|
||||
self.options.stroke.primary_working_color = primary;
|
||||
self.options.stroke.secondary_working_color = secondary;
|
||||
self.options.fill.primary_working_color = primary;
|
||||
self.options.fill.secondary_working_color = secondary;
|
||||
FreehandOptionsUpdate::StrokeEnabled(enabled) => {
|
||||
apply_stroke_enabled(&mut self.options.drawing, enabled, context.global_tool_data, context.document, responses);
|
||||
}
|
||||
FreehandOptionsUpdate::SwapFillAndStroke => {
|
||||
swap_fill_and_stroke(&mut self.options.drawing, context.document, responses);
|
||||
}
|
||||
FreehandOptionsUpdate::WorkingColorsChanged => {
|
||||
apply_working_colors(&mut self.options.drawing, context.global_tool_data, context.document);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -255,7 +265,6 @@ impl Fsm for FreehandToolFsmState {
|
|||
) -> Self {
|
||||
let ToolActionMessageContext {
|
||||
document,
|
||||
global_tool_data,
|
||||
input,
|
||||
shape_editor,
|
||||
viewport,
|
||||
|
|
@ -274,7 +283,7 @@ impl Fsm for FreehandToolFsmState {
|
|||
|
||||
tool_data.dragged = false;
|
||||
tool_data.end_point = None;
|
||||
tool_data.weight = tool_options.line_weight;
|
||||
tool_data.weight = tool_options.drawing.effective_line_weight();
|
||||
tool_data.new_layer_viewport_start = None;
|
||||
|
||||
// Extend an endpoint of the selected path
|
||||
|
|
@ -313,8 +322,8 @@ impl Fsm for FreehandToolFsmState {
|
|||
let nodes = vec![(NodeId(0), node)];
|
||||
|
||||
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
|
||||
tool_options.stroke.apply_stroke(tool_data.weight, layer, responses);
|
||||
tool_options.fill.apply_fill(layer, responses);
|
||||
tool_options.drawing.stroke.apply_stroke(tool_data.weight, layer, responses);
|
||||
tool_options.drawing.fill.apply_fill(layer, responses);
|
||||
tool_data.layer = Some(layer);
|
||||
tool_data.new_layer_viewport_start = Some(input.mouse.position);
|
||||
|
||||
|
|
@ -381,7 +390,7 @@ impl Fsm for FreehandToolFsmState {
|
|||
}
|
||||
(_, FreehandToolMessage::WorkingColorChanged) => {
|
||||
responses.add(FreehandToolMessage::UpdateOptions {
|
||||
options: FreehandOptionsUpdate::WorkingColors(Some(global_tool_data.primary_color), Some(global_tool_data.secondary_color)),
|
||||
options: FreehandOptionsUpdate::WorkingColorsChanged,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
|
|
|||
|
|
@ -222,23 +222,30 @@ impl LayoutHolder for GradientTool {
|
|||
.selected_index(Some((self.options.gradient_type == GradientType::Radial) as u32))
|
||||
.widget_instance();
|
||||
|
||||
let stops_value = self.data.current_gradient_stops.clone().map(FillChoice::Gradient).unwrap_or_else(|| {
|
||||
FillChoice::Gradient(GradientStops::new([
|
||||
GradientStop {
|
||||
position: 0.,
|
||||
midpoint: 0.5,
|
||||
color: self.data.primary_color,
|
||||
},
|
||||
GradientStop {
|
||||
position: 1.,
|
||||
midpoint: 0.5,
|
||||
color: self.data.secondary_color,
|
||||
},
|
||||
]))
|
||||
});
|
||||
// Display priority: the selected layer's stops, then any user-customized tool default, then the working colors
|
||||
let stops_value = self
|
||||
.data
|
||||
.current_gradient_stops
|
||||
.clone()
|
||||
.or_else(|| self.data.default_gradient_stops.clone())
|
||||
.map(FillChoice::Gradient)
|
||||
.unwrap_or_else(|| {
|
||||
FillChoice::Gradient(GradientStops::new([
|
||||
GradientStop {
|
||||
position: 0.,
|
||||
midpoint: 0.5,
|
||||
color: self.data.primary_color,
|
||||
},
|
||||
GradientStop {
|
||||
position: 1.,
|
||||
midpoint: 0.5,
|
||||
color: self.data.secondary_color,
|
||||
},
|
||||
]))
|
||||
});
|
||||
let stops_widget = ColorInput::new(stops_value)
|
||||
.allow_none(false)
|
||||
.disabled(!self.data.has_selected_gradient)
|
||||
.narrow(true)
|
||||
.tooltip_label("Gradient Stops")
|
||||
.tooltip_description("Edit the gradient's color stops.")
|
||||
.on_update(|input: &ColorInput| {
|
||||
|
|
@ -786,9 +793,13 @@ struct GradientToolData {
|
|||
auto_pan_shift: DVec2,
|
||||
gradient_angle: f64,
|
||||
has_selected_gradient: bool,
|
||||
/// Cached stops of the currently selected layer's gradient, mirrored into the control-bar widget. Independent of any
|
||||
/// in-progress drag (which uses `selected_gradient`) so it stays current after selection changes too.
|
||||
/// Cached stops of the currently selected layer's gradient, mirrored into the control-bar widget.
|
||||
/// Independent of any in-progress drag (which uses `selected_gradient`) so it stays current after selection changes too.
|
||||
current_gradient_stops: Option<GradientStops>,
|
||||
/// User-customized default gradient stop colors: used when nothing that has a gradient is selected.
|
||||
/// `None` means to follow the working colors.
|
||||
/// Cleared on tool deactivation so each fresh activation starts from the working colors again.
|
||||
default_gradient_stops: Option<GradientStops>,
|
||||
/// Cached viewport-space orientation (true = predominantly rightward) of the selected gradient line.
|
||||
/// Used to refresh the control bar's "Reverse Direction" icon only when the line's apparent direction flips.
|
||||
gradient_orientation_rightward: bool,
|
||||
|
|
@ -1545,6 +1556,8 @@ impl Fsm for GradientToolFsmState {
|
|||
}
|
||||
(_, GradientToolMessage::Abort) => {
|
||||
dismiss_color_stop_color_picker(tool_data, responses);
|
||||
// Clear the tool-default gradient override so re-activating the tool starts fresh from the working colors
|
||||
tool_data.default_gradient_stops = None;
|
||||
|
||||
GradientToolFsmState::Ready {
|
||||
hovering: GradientHoverTarget::None,
|
||||
|
|
@ -1785,6 +1798,7 @@ fn apply_stops_update(data: &mut GradientToolData, context: &mut ToolActionMessa
|
|||
.selected_visible_layers(&context.document.network_interface)
|
||||
.collect();
|
||||
|
||||
let mut updated_any_layer = false;
|
||||
for layer in selected_layers {
|
||||
if NodeGraphLayer::is_raster_layer(layer, &mut context.document.network_interface) {
|
||||
continue;
|
||||
|
|
@ -1792,20 +1806,30 @@ fn apply_stops_update(data: &mut GradientToolData, context: &mut ToolActionMessa
|
|||
|
||||
if get_gradient_stops(layer, &context.document.network_interface).is_some() {
|
||||
responses.add(GraphOperationMessage::GradientStopsSet { layer, stops: stops.clone() });
|
||||
updated_any_layer = true;
|
||||
} else if let Some(mut gradient) = get_gradient(layer, &context.document.network_interface) {
|
||||
gradient.stops = stops.clone();
|
||||
responses.add(GraphOperationMessage::FillSet {
|
||||
layer,
|
||||
fill: Fill::Gradient(gradient),
|
||||
});
|
||||
updated_any_layer = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(selected_gradient) = &mut data.selected_gradient {
|
||||
selected_gradient.gradient.stops = stops;
|
||||
selected_gradient.gradient.stops = stops.clone();
|
||||
}
|
||||
|
||||
// When no selected layer had a gradient to update, the user is editing the tool's default gradient instead.
|
||||
// Save those stops so the widget keeps showing them until the tool is deactivated.
|
||||
if !updated_any_layer {
|
||||
data.default_gradient_stops = Some(stops);
|
||||
}
|
||||
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
// Refresh the tool options so the swatch's `chosen_gradient` (precomputed CSS string) updates live as the user edits stops in the picker.
|
||||
responses.add(ToolMessage::RefreshToolOptions);
|
||||
}
|
||||
|
||||
/// Find the first selected visible layer that has a gradient and return both the layer ID and its resolved gradient.
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ impl LayoutHolder for PathTool {
|
|||
let x_location = NumberInput::new(x)
|
||||
.unit(" px")
|
||||
.label("X")
|
||||
.min_width(120)
|
||||
.min_width(80)
|
||||
.disabled(x.is_none())
|
||||
.min(-((1_u64 << f64::MANTISSA_DIGITS) as f64))
|
||||
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
|
|
@ -230,7 +230,7 @@ impl LayoutHolder for PathTool {
|
|||
let y_location = NumberInput::new(y)
|
||||
.unit(" px")
|
||||
.label("Y")
|
||||
.min_width(120)
|
||||
.min_width(80)
|
||||
.disabled(y.is_none())
|
||||
.min(-((1_u64 << f64::MANTISSA_DIGITS) as f64))
|
||||
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use super::tool_prelude::*;
|
||||
use crate::consts::{COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_05, DEFAULT_STROKE_WIDTH, HIDE_HANDLE_DISTANCE, LINE_ROTATE_SNAP_ANGLE, SEGMENT_OVERLAY_SIZE};
|
||||
use crate::consts::{COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_05, HIDE_HANDLE_DISTANCE, LINE_ROTATE_SNAP_ANGLE, SEGMENT_OVERLAY_SIZE};
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::MouseKeys;
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_network_node_type;
|
||||
|
|
@ -7,7 +7,10 @@ use crate::messages::portfolio::document::overlays::utility_functions::path_over
|
|||
use crate::messages::portfolio::document::overlays::utility_types::{DrawHandles, OverlayContext};
|
||||
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::{ToolColorOptions, ToolColorType};
|
||||
use crate::messages::tool::common_functionality::color_selector::{
|
||||
DrawingToolState, apply_fill_color_pick, apply_fill_enabled, apply_line_weight, apply_stroke_color_pick, apply_stroke_enabled, apply_working_colors, reset_colors_on_deactivation,
|
||||
swap_fill_and_stroke, sync_drawing_state,
|
||||
};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, merge_layers};
|
||||
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
|
||||
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
|
||||
|
|
@ -16,6 +19,7 @@ use graph_craft::document::NodeId;
|
|||
use graphene_std::Color;
|
||||
use graphene_std::subpath::pathseg_points;
|
||||
use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point};
|
||||
use graphene_std::vector::style::FillChoice;
|
||||
use graphene_std::vector::{NoHashBuilder, PointId, SegmentId, StrokeId, Vector, VectorModificationType};
|
||||
use kurbo::{CubicBez, PathSeg};
|
||||
|
||||
|
|
@ -27,18 +31,14 @@ pub struct PenTool {
|
|||
}
|
||||
|
||||
pub struct PenOptions {
|
||||
line_weight: f64,
|
||||
fill: ToolColorOptions,
|
||||
stroke: ToolColorOptions,
|
||||
drawing: DrawingToolState,
|
||||
pen_overlay_mode: PenOverlayMode,
|
||||
}
|
||||
|
||||
impl Default for PenOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
line_weight: DEFAULT_STROKE_WIDTH,
|
||||
fill: ToolColorOptions::new_secondary(),
|
||||
stroke: ToolColorOptions::new_primary(),
|
||||
drawing: DrawingToolState::new(true),
|
||||
pen_overlay_mode: PenOverlayMode::FrontierHandles,
|
||||
}
|
||||
}
|
||||
|
|
@ -119,12 +119,13 @@ pub enum PenOverlayMode {
|
|||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum PenOptionsUpdate {
|
||||
FillColor(Option<Color>),
|
||||
FillColorType(ToolColorType),
|
||||
FillColor(FillChoice),
|
||||
FillEnabled(bool),
|
||||
LineWeight(f64),
|
||||
StrokeColor(Option<Color>),
|
||||
StrokeColorType(ToolColorType),
|
||||
WorkingColors(Option<Color>, Option<Color>),
|
||||
StrokeEnabled(bool),
|
||||
SwapFillAndStroke,
|
||||
WorkingColorsChanged,
|
||||
OverlayModeType(PenOverlayMode),
|
||||
}
|
||||
|
||||
|
|
@ -140,80 +141,83 @@ impl ToolMetadata for PenTool {
|
|||
}
|
||||
}
|
||||
|
||||
fn create_weight_widget(line_weight: f64) -> WidgetInstance {
|
||||
NumberInput::new(Some(line_weight))
|
||||
fn create_weight_widget(line_weight: Option<f64>, disabled: bool) -> WidgetInstance {
|
||||
NumberInput::new(line_weight)
|
||||
.unit(" px")
|
||||
.label("Weight")
|
||||
.min(0.)
|
||||
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
.min_width(100)
|
||||
.narrow(true)
|
||||
.disabled(disabled)
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
PenToolMessage::UpdateOptions {
|
||||
options: PenOptionsUpdate::LineWeight(number_input.value.unwrap()),
|
||||
if let Some(value) = number_input.value {
|
||||
PenToolMessage::UpdateOptions {
|
||||
options: PenOptionsUpdate::LineWeight(value),
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
Message::NoOp
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.on_commit(|_| DocumentMessage::StartTransaction.into())
|
||||
.widget_instance()
|
||||
}
|
||||
|
||||
impl LayoutHolder for PenTool {
|
||||
fn layout(&self) -> Layout {
|
||||
let mut widgets = self.options.fill.create_widgets(
|
||||
"Fill",
|
||||
true,
|
||||
|_| {
|
||||
let mut widgets = self.options.drawing.fill.create_widgets(
|
||||
"Fill:",
|
||||
|checkbox: &CheckboxInput| {
|
||||
PenToolMessage::UpdateOptions {
|
||||
options: PenOptionsUpdate::FillColor(None),
|
||||
options: PenOptionsUpdate::FillEnabled(checkbox.checked),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
|color_type: ToolColorType| {
|
||||
WidgetCallback::new(move |_| {
|
||||
PenToolMessage::UpdateOptions {
|
||||
options: PenOptionsUpdate::FillColorType(color_type.clone()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
},
|
||||
|color: &ColorInput| {
|
||||
PenToolMessage::UpdateOptions {
|
||||
options: PenOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
|
||||
options: PenOptionsUpdate::FillColor(color.value.clone()),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
);
|
||||
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
|
||||
widgets.append(&mut self.options.stroke.create_widgets(
|
||||
"Stroke",
|
||||
true,
|
||||
|_| {
|
||||
PenToolMessage::UpdateOptions {
|
||||
options: PenOptionsUpdate::StrokeColor(None),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
|color_type: ToolColorType| {
|
||||
WidgetCallback::new(move |_| {
|
||||
widgets.push(
|
||||
IconButton::new("SwapHorizontal", 16)
|
||||
.tooltip_label("Swap Fill/Stroke Colors")
|
||||
.on_update(|_| {
|
||||
PenToolMessage::UpdateOptions {
|
||||
options: PenOptionsUpdate::StrokeColorType(color_type.clone()),
|
||||
options: PenOptionsUpdate::SwapFillAndStroke,
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_instance(),
|
||||
);
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
|
||||
widgets.append(&mut self.options.drawing.stroke.create_widgets(
|
||||
"Stroke:",
|
||||
|checkbox: &CheckboxInput| {
|
||||
PenToolMessage::UpdateOptions {
|
||||
options: PenOptionsUpdate::StrokeEnabled(checkbox.checked),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
|color: &ColorInput| {
|
||||
PenToolMessage::UpdateOptions {
|
||||
options: PenOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
|
||||
options: PenOptionsUpdate::StrokeColor(color.value.as_solid()),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
));
|
||||
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
widgets.push(Separator::new(SeparatorStyle::Related).widget_instance());
|
||||
|
||||
widgets.push(create_weight_widget(self.options.line_weight));
|
||||
let weight_disabled = self.options.drawing.stroke.enabled == Some(false);
|
||||
widgets.push(create_weight_widget(self.options.drawing.line_weight, weight_disabled));
|
||||
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
widgets.push(Separator::new(SeparatorStyle::Section).widget_instance());
|
||||
|
||||
widgets.push(
|
||||
RadioInput::new(vec![
|
||||
|
|
@ -249,12 +253,17 @@ impl LayoutHolder for PenTool {
|
|||
#[message_handler_data]
|
||||
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for PenTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
|
||||
// On tool deactivation (Abort fires from the dispatcher's tool transition), reset the displayed fill/stroke colors so
|
||||
// the next activation starts fresh from the current working colors. The global swap state persists across tool switches.
|
||||
// Guarded on `Ready` so Esc-mid-drawing (which also fires Abort) doesn't wipe the user's customized fill/stroke options.
|
||||
if matches!(&message, ToolMessage::Pen(PenToolMessage::Abort)) && self.fsm_state == PenToolFsmState::Ready {
|
||||
reset_colors_on_deactivation(&mut self.options.drawing, context.global_tool_data);
|
||||
}
|
||||
|
||||
if matches!(&message, ToolMessage::Pen(PenToolMessage::SelectionChanged))
|
||||
&& self.fsm_state == PenToolFsmState::Ready
|
||||
&& let Some(weight) = graph_modification_utils::first_selected_stroke_weight(context.document)
|
||||
&& self.options.line_weight != weight
|
||||
&& sync_drawing_state(&mut self.options.drawing, true, true, context.global_tool_data, context.document)
|
||||
{
|
||||
self.options.line_weight = weight;
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
}
|
||||
|
||||
|
|
@ -269,24 +278,25 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for PenT
|
|||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
PenOptionsUpdate::LineWeight(line_weight) => {
|
||||
self.options.line_weight = line_weight;
|
||||
graph_modification_utils::set_stroke_weight_for_selected_layers(line_weight, context.document, responses);
|
||||
apply_line_weight(&mut self.options.drawing, line_weight, context.document, responses);
|
||||
}
|
||||
PenOptionsUpdate::FillColor(color) => {
|
||||
self.options.fill.custom_color = color;
|
||||
self.options.fill.color_type = ToolColorType::Custom;
|
||||
PenOptionsUpdate::FillColor(fill_choice) => {
|
||||
apply_fill_color_pick(&mut self.options.drawing, fill_choice, context.document, responses);
|
||||
}
|
||||
PenOptionsUpdate::FillEnabled(enabled) => {
|
||||
apply_fill_enabled(&mut self.options.drawing, enabled, context.global_tool_data, context.document, responses);
|
||||
}
|
||||
PenOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type,
|
||||
PenOptionsUpdate::StrokeColor(color) => {
|
||||
self.options.stroke.custom_color = color;
|
||||
self.options.stroke.color_type = ToolColorType::Custom;
|
||||
apply_stroke_color_pick(&mut self.options.drawing, color, context.document, responses);
|
||||
}
|
||||
PenOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type,
|
||||
PenOptionsUpdate::WorkingColors(primary, secondary) => {
|
||||
self.options.stroke.primary_working_color = primary;
|
||||
self.options.stroke.secondary_working_color = secondary;
|
||||
self.options.fill.primary_working_color = primary;
|
||||
self.options.fill.secondary_working_color = secondary;
|
||||
PenOptionsUpdate::StrokeEnabled(enabled) => {
|
||||
apply_stroke_enabled(&mut self.options.drawing, enabled, context.global_tool_data, context.document, responses);
|
||||
}
|
||||
PenOptionsUpdate::SwapFillAndStroke => {
|
||||
swap_fill_and_stroke(&mut self.options.drawing, context.document, responses);
|
||||
}
|
||||
PenOptionsUpdate::WorkingColorsChanged => {
|
||||
apply_working_colors(&mut self.options.drawing, context.global_tool_data, context.document);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1306,8 +1316,8 @@ impl PenToolData {
|
|||
let parent = document.new_layer_bounding_artboard(input, viewport);
|
||||
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
|
||||
self.current_layer = Some(layer);
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
|
||||
tool_options.fill.apply_fill(layer, responses);
|
||||
tool_options.drawing.stroke.apply_stroke(tool_options.drawing.effective_line_weight(), layer, responses);
|
||||
tool_options.drawing.fill.apply_fill(layer, responses);
|
||||
self.prior_segment = None;
|
||||
self.prior_segments = None;
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] });
|
||||
|
|
@ -1490,7 +1500,6 @@ impl Fsm for PenToolFsmState {
|
|||
) -> Self {
|
||||
let ToolActionMessageContext {
|
||||
document,
|
||||
global_tool_data,
|
||||
input,
|
||||
shape_editor,
|
||||
viewport,
|
||||
|
|
@ -1839,7 +1848,7 @@ impl Fsm for PenToolFsmState {
|
|||
}
|
||||
(_, PenToolMessage::WorkingColorChanged) => {
|
||||
responses.add(PenToolMessage::UpdateOptions {
|
||||
options: PenOptionsUpdate::WorkingColors(Some(global_tool_data.primary_color), Some(global_tool_data.secondary_color)),
|
||||
options: PenOptionsUpdate::WorkingColorsChanged,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
use super::tool_prelude::*;
|
||||
use crate::consts::{BOUNDS_SELECT_THRESHOLD, DEFAULT_STROKE_WIDTH, SNAP_POINT_TOLERANCE};
|
||||
use crate::consts::{BOUNDS_SELECT_THRESHOLD, SNAP_POINT_TOLERANCE};
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
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::{ToolColorOptions, ToolColorType};
|
||||
use crate::messages::tool::common_functionality::color_selector::{
|
||||
DrawingToolState, apply_fill_color_pick, apply_fill_enabled, apply_line_weight, apply_stroke_color_pick, apply_stroke_enabled, apply_working_colors, has_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;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::common_functionality::resize::Resize;
|
||||
|
|
@ -22,10 +25,12 @@ use crate::messages::tool::common_functionality::shapes::{Ellipse, Line, Rectang
|
|||
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapTypeConfiguration};
|
||||
use crate::messages::tool::common_functionality::transformation_cage::{BoundingBoxManager, EdgeBool};
|
||||
use crate::messages::tool::common_functionality::utility_functions::{closest_point, resize_bounds, rotate_bounds, skew_bounds, transforming_transform_cage};
|
||||
use crate::messages::tool::utility_types::DocumentToolData;
|
||||
use graph_craft::document::NodeId;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graphene_std::renderer::Quad;
|
||||
use graphene_std::vector::misc::{ArcType, GridType, SpiralType};
|
||||
use graphene_std::vector::style::FillChoice;
|
||||
use graphene_std::{Color, NodeInputDecleration};
|
||||
use std::vec;
|
||||
|
||||
|
|
@ -37,9 +42,17 @@ pub struct ShapeTool {
|
|||
}
|
||||
|
||||
pub struct ShapeToolOptions {
|
||||
line_weight: f64,
|
||||
fill: ToolColorOptions,
|
||||
stroke: ToolColorOptions,
|
||||
drawing: DrawingToolState,
|
||||
/// Per-shape-mode default for whether the fill checkbox is ticked when no layer is selected. Initialized from
|
||||
/// [`ShapeType::defaults_to_fill`] and updated when the user toggles the fill checkbox while nothing is selected,
|
||||
/// so the preference for each mode persists across mode switches and selection changes.
|
||||
shape_fill_defaults: std::collections::HashMap<ShapeType, bool>,
|
||||
/// Per-shape-mode default for whether the stroke checkbox is ticked when no layer is selected.
|
||||
/// Initialized to `true` for every mode and updated when the user toggles the stroke checkbox while nothing is selected.
|
||||
shape_stroke_defaults: std::collections::HashMap<ShapeType, bool>,
|
||||
/// Per-shape-mode value of the fill/stroke swap flag (mirrors `drawing.colors_swapped` for the current shape mode).
|
||||
/// Updated whenever the user toggles swap; read back when changing shape modes so each alias remembers its own routing.
|
||||
shape_colors_swapped: std::collections::HashMap<ShapeType, bool>,
|
||||
vertices: u32,
|
||||
shape_type: ShapeType,
|
||||
arc_type: ArcType,
|
||||
|
|
@ -53,10 +66,15 @@ pub struct ShapeToolOptions {
|
|||
|
||||
impl Default for ShapeToolOptions {
|
||||
fn default() -> Self {
|
||||
let shape_fill_defaults = ShapeType::ALL.iter().map(|&shape| (shape, shape.defaults_to_fill())).collect();
|
||||
let shape_stroke_defaults = ShapeType::ALL.iter().map(|&shape| (shape, true)).collect();
|
||||
let shape_colors_swapped = ShapeType::ALL.iter().map(|&shape| (shape, false)).collect();
|
||||
|
||||
Self {
|
||||
line_weight: DEFAULT_STROKE_WIDTH,
|
||||
fill: ToolColorOptions::new_secondary(),
|
||||
stroke: ToolColorOptions::new_primary(),
|
||||
drawing: DrawingToolState::new(true),
|
||||
shape_fill_defaults,
|
||||
shape_stroke_defaults,
|
||||
shape_colors_swapped,
|
||||
vertices: 5,
|
||||
shape_type: ShapeType::Polygon,
|
||||
arc_type: ArcType::Open,
|
||||
|
|
@ -73,12 +91,13 @@ impl Default for ShapeToolOptions {
|
|||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ShapeOptionsUpdate {
|
||||
FillColor(Option<Color>),
|
||||
FillColorType(ToolColorType),
|
||||
FillColor(FillChoice),
|
||||
FillEnabled(bool),
|
||||
LineWeight(f64),
|
||||
StrokeColor(Option<Color>),
|
||||
StrokeColorType(ToolColorType),
|
||||
WorkingColors(Option<Color>, Option<Color>),
|
||||
StrokeEnabled(bool),
|
||||
SwapFillAndStroke,
|
||||
WorkingColorsChanged,
|
||||
Vertices(u32),
|
||||
ShapeType(ShapeType),
|
||||
ArcType(ArcType),
|
||||
|
|
@ -220,18 +239,26 @@ fn create_arc_type_widget(arc_type: ArcType) -> WidgetInstance {
|
|||
RadioInput::new(entries).selected_index(Some(arc_type as u32)).widget_instance()
|
||||
}
|
||||
|
||||
fn create_weight_widget(line_weight: f64) -> WidgetInstance {
|
||||
NumberInput::new(Some(line_weight))
|
||||
fn create_weight_widget(line_weight: Option<f64>, disabled: bool) -> WidgetInstance {
|
||||
NumberInput::new(line_weight)
|
||||
.unit(" px")
|
||||
.label("Weight")
|
||||
.min(0.)
|
||||
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
.min_width(100)
|
||||
.narrow(true)
|
||||
.disabled(disabled)
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::LineWeight(number_input.value.unwrap()),
|
||||
if let Some(value) = number_input.value {
|
||||
ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::LineWeight(value),
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
Message::NoOp
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.on_commit(|_| DocumentMessage::StartTransaction.into())
|
||||
.widget_instance()
|
||||
}
|
||||
|
||||
|
|
@ -431,102 +458,113 @@ fn sync_shape_options_from_selection(options: &mut ShapeToolOptions, tool_data:
|
|||
changed
|
||||
}
|
||||
|
||||
/// Shared logic for handling a shape-mode change from either the `SetShape` alias (FSM-driven) or the `ShapeType` dropdown.
|
||||
/// Loads the new mode's persistent swap flag, resets the displayed fill/stroke colors back to the (now routed) working colors,
|
||||
/// and re-syncs from the selection using the new mode's natural fill/stroke defaults. The caller is responsible for updating
|
||||
/// `options.shape_type` and `tool_data.current_shape` beforehand if needed.
|
||||
fn handle_shape_mode_change(options: &mut ShapeToolOptions, new_shape: ShapeType, prev_shape: ShapeType, global: &DocumentToolData, document: &DocumentMessageHandler) {
|
||||
if new_shape != prev_shape {
|
||||
options.drawing.colors_swapped = *options.shape_colors_swapped.get(&new_shape).unwrap_or(&false);
|
||||
reset_colors_on_deactivation(&mut options.drawing, global);
|
||||
}
|
||||
let natural_fill_enabled = *options.shape_fill_defaults.get(&new_shape).unwrap_or(&new_shape.defaults_to_fill());
|
||||
let natural_stroke_enabled = *options.shape_stroke_defaults.get(&new_shape).unwrap_or(&true);
|
||||
// Treat the shape change as a real selection change so the new mode's natural defaults apply when nothing matches on the selection.
|
||||
sync_color_options(&mut options.drawing, natural_fill_enabled, natural_stroke_enabled, global, document, true);
|
||||
}
|
||||
|
||||
impl LayoutHolder for ShapeTool {
|
||||
fn layout(&self) -> Layout {
|
||||
let mut widgets = vec![];
|
||||
|
||||
if !self.tool_data.hide_shape_option_widget {
|
||||
widgets.push(create_shape_option_widget(self.options.shape_type));
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
|
||||
if self.options.shape_type == ShapeType::Polygon || self.options.shape_type == ShapeType::Star {
|
||||
widgets.push(create_sides_widget(self.options.vertices));
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
}
|
||||
|
||||
if self.options.shape_type == ShapeType::Arc {
|
||||
widgets.push(create_arc_type_widget(self.options.arc_type));
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
}
|
||||
}
|
||||
|
||||
if self.options.shape_type == ShapeType::Spiral {
|
||||
widgets.push(create_spiral_type_widget(self.options.spiral_type));
|
||||
widgets.push(Separator::new(SeparatorStyle::Related).widget_instance());
|
||||
|
||||
widgets.push(create_turns_widget(self.options.turns));
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
}
|
||||
|
||||
if self.options.shape_type == ShapeType::Grid {
|
||||
widgets.push(create_grid_type_widget(self.options.grid_type));
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
}
|
||||
|
||||
if self.options.shape_type == ShapeType::Arrow {
|
||||
widgets.push(create_arrow_shaft_width_widget(self.options.arrow_shaft_width));
|
||||
widgets.push(Separator::new(SeparatorStyle::Related).widget_instance());
|
||||
widgets.push(create_arrow_head_width_widget(self.options.arrow_head_width));
|
||||
widgets.push(Separator::new(SeparatorStyle::Related).widget_instance());
|
||||
widgets.push(create_arrow_head_length_widget(self.options.arrow_head_length));
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
}
|
||||
|
||||
// Fill / Stroke / Weight (Shared across all shape modes. Line shows no Fill)
|
||||
if self.options.shape_type != ShapeType::Line {
|
||||
widgets.append(&mut self.options.fill.create_widgets(
|
||||
"Fill",
|
||||
true,
|
||||
|_| {
|
||||
widgets.append(&mut self.options.drawing.fill.create_widgets(
|
||||
"Fill:",
|
||||
|checkbox: &CheckboxInput| {
|
||||
ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::FillColor(None),
|
||||
options: ShapeOptionsUpdate::FillEnabled(checkbox.checked),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
|color_type: ToolColorType| {
|
||||
WidgetCallback::new(move |_| {
|
||||
ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::FillColorType(color_type.clone()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
},
|
||||
|color: &ColorInput| {
|
||||
ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
|
||||
options: ShapeOptionsUpdate::FillColor(color.value.clone()),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
));
|
||||
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
widgets.push(
|
||||
IconButton::new("SwapHorizontal", 16)
|
||||
.tooltip_label("Swap Fill/Stroke Colors")
|
||||
.on_update(|_| {
|
||||
ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::SwapFillAndStroke,
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_instance(),
|
||||
);
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
}
|
||||
|
||||
widgets.append(&mut self.options.stroke.create_widgets(
|
||||
"Stroke",
|
||||
true,
|
||||
|_| {
|
||||
widgets.append(&mut self.options.drawing.stroke.create_widgets(
|
||||
"Stroke:",
|
||||
|checkbox: &CheckboxInput| {
|
||||
ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::StrokeColor(None),
|
||||
options: ShapeOptionsUpdate::StrokeEnabled(checkbox.checked),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
|color_type: ToolColorType| {
|
||||
WidgetCallback::new(move |_| {
|
||||
ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::StrokeColorType(color_type.clone()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
},
|
||||
|color: &ColorInput| {
|
||||
ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
|
||||
options: ShapeOptionsUpdate::StrokeColor(color.value.as_solid()),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
));
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
widgets.push(create_weight_widget(self.options.line_weight));
|
||||
widgets.push(Separator::new(SeparatorStyle::Related).widget_instance());
|
||||
let weight_disabled = self.options.drawing.stroke.enabled == Some(false);
|
||||
widgets.push(create_weight_widget(self.options.drawing.line_weight, weight_disabled));
|
||||
|
||||
// Shape-mode dropdown and per-shape parameters
|
||||
if !self.tool_data.hide_shape_option_widget {
|
||||
widgets.push(Separator::new(SeparatorStyle::Section).widget_instance());
|
||||
widgets.push(create_shape_option_widget(self.options.shape_type));
|
||||
|
||||
if self.options.shape_type == ShapeType::Polygon || self.options.shape_type == ShapeType::Star {
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
widgets.push(create_sides_widget(self.options.vertices));
|
||||
}
|
||||
|
||||
if self.options.shape_type == ShapeType::Arc {
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
widgets.push(create_arc_type_widget(self.options.arc_type));
|
||||
}
|
||||
|
||||
if self.options.shape_type == ShapeType::Spiral {
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
widgets.push(create_spiral_type_widget(self.options.spiral_type));
|
||||
widgets.push(Separator::new(SeparatorStyle::Related).widget_instance());
|
||||
widgets.push(create_turns_widget(self.options.turns));
|
||||
}
|
||||
|
||||
if self.options.shape_type == ShapeType::Grid {
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
widgets.push(create_grid_type_widget(self.options.grid_type));
|
||||
}
|
||||
|
||||
if self.options.shape_type == ShapeType::Arrow {
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
widgets.push(create_arrow_shaft_width_widget(self.options.arrow_shaft_width));
|
||||
widgets.push(Separator::new(SeparatorStyle::Related).widget_instance());
|
||||
widgets.push(create_arrow_head_width_widget(self.options.arrow_head_width));
|
||||
widgets.push(Separator::new(SeparatorStyle::Related).widget_instance());
|
||||
widgets.push(create_arrow_head_length_widget(self.options.arrow_head_length));
|
||||
}
|
||||
}
|
||||
|
||||
Layout(vec![LayoutGroup::row(widgets)])
|
||||
}
|
||||
|
|
@ -537,20 +575,23 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Shap
|
|||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
|
||||
use graphene_std::vector::generator_nodes::*;
|
||||
|
||||
// On tool deactivation (Abort fires from the dispatcher's tool transition), reset the displayed fill/stroke colors so
|
||||
// the next activation starts fresh from the current working colors. The global swap state persists across tool switches.
|
||||
// Guarded on `Ready(_)` so Esc-mid-drawing (which also fires Abort) doesn't wipe the user's customized fill/stroke options.
|
||||
if matches!(&message, ToolMessage::Shape(ShapeToolMessage::Abort)) && matches!(self.fsm_state, ShapeToolFsmState::Ready(_)) {
|
||||
reset_colors_on_deactivation(&mut self.options.drawing, context.global_tool_data);
|
||||
}
|
||||
|
||||
if matches!(&message, ToolMessage::Shape(ShapeToolMessage::SelectionChanged)) {
|
||||
if !matches!(self.fsm_state, ShapeToolFsmState::Ready(_)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut needs_refresh = false;
|
||||
|
||||
// Stroke weight is shape-agnostic. Sync it regardless of which (if any) shape proto node the layer has.
|
||||
if let Some(weight) = graph_modification_utils::first_selected_stroke_weight(context.document)
|
||||
&& self.options.line_weight != weight
|
||||
{
|
||||
self.options.line_weight = weight;
|
||||
needs_refresh = true;
|
||||
}
|
||||
// The natural fill/stroke defaults depend on the shape type (Spiral/Grid/Line have no fill by default).
|
||||
let current_shape = self.tool_data.current_shape;
|
||||
let natural_fill_enabled = *self.options.shape_fill_defaults.get(¤t_shape).unwrap_or(¤t_shape.defaults_to_fill());
|
||||
let natural_stroke_enabled = *self.options.shape_stroke_defaults.get(¤t_shape).unwrap_or(&true);
|
||||
let mut needs_refresh = sync_drawing_state(&mut self.options.drawing, natural_fill_enabled, natural_stroke_enabled, context.global_tool_data, context.document);
|
||||
|
||||
// Detect which shape the first selected layer is by checking for each generator's proto node, then mirror
|
||||
// the control bar's `shape_type` into that and pull the shape's parameters into the matching control bar fields.
|
||||
|
|
@ -562,38 +603,57 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Shap
|
|||
return;
|
||||
}
|
||||
|
||||
// SetShape changes the active shape mode, which can change the natural fill default (e.g. Line/Spiral/Grid have no fill).
|
||||
// Trigger a re-sync afterward so the controls reflect either the current selection or the new natural default.
|
||||
// Note: the `UpdateOptions { ShapeType(_) }` variant matches the `let else` below and is handled by the `ShapeType` arm,
|
||||
// so it can't reach the `else` block where this flag is read — only `SetShape` (the FSM-routed alias) can.
|
||||
let is_set_shape = matches!(&message, ToolMessage::Shape(ShapeToolMessage::SetShape { .. }));
|
||||
let shape_before = self.tool_data.current_shape;
|
||||
|
||||
let ToolMessage::Shape(ShapeToolMessage::UpdateOptions { options }) = message else {
|
||||
self.fsm_state.process_event(message, &mut self.tool_data, context, &self.options, responses, true);
|
||||
if is_set_shape {
|
||||
handle_shape_mode_change(&mut self.options, self.tool_data.current_shape, shape_before, context.global_tool_data, context.document);
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
}
|
||||
return;
|
||||
};
|
||||
match options {
|
||||
ShapeOptionsUpdate::FillColor(color) => {
|
||||
self.options.fill.custom_color = color;
|
||||
self.options.fill.color_type = ToolColorType::Custom;
|
||||
ShapeOptionsUpdate::FillColor(fill_choice) => {
|
||||
apply_fill_color_pick(&mut self.options.drawing, fill_choice, context.document, responses);
|
||||
}
|
||||
ShapeOptionsUpdate::FillColorType(color_type) => {
|
||||
self.options.fill.color_type = color_type;
|
||||
ShapeOptionsUpdate::FillEnabled(enabled) => {
|
||||
// When toggled with no selection, persist the new state as the current shape mode's default
|
||||
if !has_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);
|
||||
}
|
||||
ShapeOptionsUpdate::LineWeight(line_weight) => {
|
||||
self.options.line_weight = line_weight;
|
||||
graph_modification_utils::set_stroke_weight_for_selected_layers(line_weight, context.document, responses);
|
||||
apply_line_weight(&mut self.options.drawing, line_weight, context.document, responses);
|
||||
}
|
||||
ShapeOptionsUpdate::StrokeColor(color) => {
|
||||
self.options.stroke.custom_color = color;
|
||||
self.options.stroke.color_type = ToolColorType::Custom;
|
||||
apply_stroke_color_pick(&mut self.options.drawing, color, context.document, responses);
|
||||
}
|
||||
ShapeOptionsUpdate::StrokeColorType(color_type) => {
|
||||
self.options.stroke.color_type = color_type;
|
||||
ShapeOptionsUpdate::StrokeEnabled(enabled) => {
|
||||
// When toggled with no selection, persist the new state as the current shape mode's default
|
||||
if !has_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);
|
||||
}
|
||||
ShapeOptionsUpdate::WorkingColors(primary, secondary) => {
|
||||
self.options.stroke.primary_working_color = primary;
|
||||
self.options.stroke.secondary_working_color = secondary;
|
||||
self.options.fill.primary_working_color = primary;
|
||||
self.options.fill.secondary_working_color = secondary;
|
||||
ShapeOptionsUpdate::SwapFillAndStroke => {
|
||||
swap_fill_and_stroke(&mut self.options.drawing, context.document, responses);
|
||||
// Persist the new swap state as the current shape mode's default
|
||||
self.options.shape_colors_swapped.insert(self.tool_data.current_shape, self.options.drawing.colors_swapped);
|
||||
}
|
||||
ShapeOptionsUpdate::WorkingColorsChanged => {
|
||||
apply_working_colors(&mut self.options.drawing, context.global_tool_data, context.document);
|
||||
}
|
||||
ShapeOptionsUpdate::ShapeType(shape) => {
|
||||
self.options.shape_type = shape;
|
||||
self.tool_data.current_shape = shape;
|
||||
handle_shape_mode_change(&mut self.options, shape, shape_before, context.global_tool_data, context.document);
|
||||
}
|
||||
ShapeOptionsUpdate::Vertices(vertices) => {
|
||||
self.options.vertices = vertices;
|
||||
|
|
@ -806,7 +866,6 @@ impl Fsm for ShapeToolFsmState {
|
|||
tool_data: &mut Self::ToolData,
|
||||
ToolActionMessageContext {
|
||||
document,
|
||||
global_tool_data,
|
||||
input,
|
||||
shape_editor,
|
||||
viewport,
|
||||
|
|
@ -1117,8 +1176,8 @@ impl Fsm for ShapeToolFsmState {
|
|||
skip_rerender: false,
|
||||
});
|
||||
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, defered_responses);
|
||||
tool_options.fill.apply_fill(layer, defered_responses);
|
||||
tool_options.drawing.stroke.apply_stroke(tool_options.drawing.effective_line_weight(), layer, defered_responses);
|
||||
tool_options.drawing.fill.apply_fill(layer, defered_responses);
|
||||
}
|
||||
ShapeType::Arrow => {
|
||||
let viewport_drag_start = tool_data.data.viewport_drag_start(document);
|
||||
|
|
@ -1129,10 +1188,10 @@ impl Fsm for ShapeToolFsmState {
|
|||
skip_rerender: false,
|
||||
});
|
||||
|
||||
tool_data.line_data.weight = tool_options.line_weight;
|
||||
tool_data.line_data.weight = tool_options.drawing.effective_line_weight();
|
||||
tool_data.line_data.editing_layer = Some(layer);
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, defered_responses);
|
||||
tool_options.fill.apply_fill(layer, defered_responses);
|
||||
tool_options.drawing.stroke.apply_stroke(tool_options.drawing.effective_line_weight(), layer, defered_responses);
|
||||
tool_options.drawing.fill.apply_fill(layer, defered_responses);
|
||||
}
|
||||
ShapeType::Line => {
|
||||
let viewport_drag_start = tool_data.data.viewport_drag_start(document);
|
||||
|
|
@ -1143,9 +1202,9 @@ impl Fsm for ShapeToolFsmState {
|
|||
skip_rerender: false,
|
||||
});
|
||||
|
||||
tool_data.line_data.weight = tool_options.line_weight;
|
||||
tool_data.line_data.weight = tool_options.drawing.effective_line_weight();
|
||||
tool_data.line_data.editing_layer = Some(layer);
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, defered_responses);
|
||||
tool_options.drawing.stroke.apply_stroke(tool_options.drawing.effective_line_weight(), layer, defered_responses);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1351,7 +1410,7 @@ impl Fsm for ShapeToolFsmState {
|
|||
}
|
||||
(_, ShapeToolMessage::WorkingColorChanged) => {
|
||||
responses.add(ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::WorkingColors(Some(global_tool_data.primary_color), Some(global_tool_data.secondary_color)),
|
||||
options: ShapeOptionsUpdate::WorkingColorsChanged,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use super::tool_prelude::*;
|
||||
use crate::consts::{DEFAULT_STROKE_WIDTH, DRAG_THRESHOLD, PATH_JOIN_THRESHOLD, SNAP_POINT_TOLERANCE};
|
||||
use crate::consts::{DRAG_THRESHOLD, PATH_JOIN_THRESHOLD, SNAP_POINT_TOLERANCE};
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::MouseKeys;
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::{resolve_network_node_type, resolve_proto_node_type};
|
||||
|
|
@ -7,12 +7,16 @@ use crate::messages::portfolio::document::overlays::utility_functions::path_endp
|
|||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
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::{ToolColorOptions, ToolColorType};
|
||||
use crate::messages::tool::common_functionality::color_selector::{
|
||||
DrawingToolState, apply_fill_color_pick, apply_fill_enabled, apply_line_weight, apply_stroke_color_pick, apply_stroke_enabled, apply_working_colors, reset_colors_on_deactivation,
|
||||
swap_fill_and_stroke, sync_drawing_state,
|
||||
};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, find_spline, merge_layers, merge_points};
|
||||
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapData, SnapManager, SnapTypeConfiguration, SnappedPoint};
|
||||
use crate::messages::tool::common_functionality::utility_functions::{closest_point, should_extend};
|
||||
use graph_craft::document::{NodeId, NodeInput};
|
||||
use graphene_std::Color;
|
||||
use graphene_std::vector::style::FillChoice;
|
||||
use graphene_std::vector::{PointId, SegmentId, VectorModificationType};
|
||||
|
||||
#[derive(Default, ExtractField)]
|
||||
|
|
@ -23,17 +27,13 @@ pub struct SplineTool {
|
|||
}
|
||||
|
||||
pub struct SplineOptions {
|
||||
line_weight: f64,
|
||||
fill: ToolColorOptions,
|
||||
stroke: ToolColorOptions,
|
||||
drawing: DrawingToolState,
|
||||
}
|
||||
|
||||
impl Default for SplineOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
line_weight: DEFAULT_STROKE_WIDTH,
|
||||
fill: ToolColorOptions::new_none(),
|
||||
stroke: ToolColorOptions::new_primary(),
|
||||
drawing: DrawingToolState::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -71,12 +71,13 @@ enum SplineToolFsmState {
|
|||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum SplineOptionsUpdate {
|
||||
FillColor(Option<Color>),
|
||||
FillColorType(ToolColorType),
|
||||
FillColor(FillChoice),
|
||||
FillEnabled(bool),
|
||||
LineWeight(f64),
|
||||
StrokeColor(Option<Color>),
|
||||
StrokeColorType(ToolColorType),
|
||||
WorkingColors(Option<Color>, Option<Color>),
|
||||
StrokeEnabled(bool),
|
||||
SwapFillAndStroke,
|
||||
WorkingColorsChanged,
|
||||
}
|
||||
|
||||
impl ToolMetadata for SplineTool {
|
||||
|
|
@ -91,76 +92,79 @@ impl ToolMetadata for SplineTool {
|
|||
}
|
||||
}
|
||||
|
||||
fn create_weight_widget(line_weight: f64) -> WidgetInstance {
|
||||
NumberInput::new(Some(line_weight))
|
||||
fn create_weight_widget(line_weight: Option<f64>, disabled: bool) -> WidgetInstance {
|
||||
NumberInput::new(line_weight)
|
||||
.unit(" px")
|
||||
.label("Weight")
|
||||
.min(0.)
|
||||
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
.min_width(100)
|
||||
.narrow(true)
|
||||
.disabled(disabled)
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
SplineToolMessage::UpdateOptions {
|
||||
options: SplineOptionsUpdate::LineWeight(number_input.value.unwrap()),
|
||||
if let Some(value) = number_input.value {
|
||||
SplineToolMessage::UpdateOptions {
|
||||
options: SplineOptionsUpdate::LineWeight(value),
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
Message::NoOp
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.on_commit(|_| DocumentMessage::StartTransaction.into())
|
||||
.widget_instance()
|
||||
}
|
||||
|
||||
impl LayoutHolder for SplineTool {
|
||||
fn layout(&self) -> Layout {
|
||||
let mut widgets = self.options.fill.create_widgets(
|
||||
"Fill",
|
||||
true,
|
||||
|_| {
|
||||
let mut widgets = self.options.drawing.fill.create_widgets(
|
||||
"Fill:",
|
||||
|checkbox: &CheckboxInput| {
|
||||
SplineToolMessage::UpdateOptions {
|
||||
options: SplineOptionsUpdate::FillColor(None),
|
||||
options: SplineOptionsUpdate::FillEnabled(checkbox.checked),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
|color_type: ToolColorType| {
|
||||
WidgetCallback::new(move |_| {
|
||||
SplineToolMessage::UpdateOptions {
|
||||
options: SplineOptionsUpdate::FillColorType(color_type.clone()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
},
|
||||
|color: &ColorInput| {
|
||||
SplineToolMessage::UpdateOptions {
|
||||
options: SplineOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
|
||||
options: SplineOptionsUpdate::FillColor(color.value.clone()),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
);
|
||||
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
|
||||
widgets.append(&mut self.options.stroke.create_widgets(
|
||||
"Stroke",
|
||||
true,
|
||||
|_| {
|
||||
SplineToolMessage::UpdateOptions {
|
||||
options: SplineOptionsUpdate::StrokeColor(None),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
|color_type: ToolColorType| {
|
||||
WidgetCallback::new(move |_| {
|
||||
widgets.push(
|
||||
IconButton::new("SwapHorizontal", 16)
|
||||
.tooltip_label("Swap Fill/Stroke Colors")
|
||||
.on_update(|_| {
|
||||
SplineToolMessage::UpdateOptions {
|
||||
options: SplineOptionsUpdate::StrokeColorType(color_type.clone()),
|
||||
options: SplineOptionsUpdate::SwapFillAndStroke,
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_instance(),
|
||||
);
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
|
||||
widgets.append(&mut self.options.drawing.stroke.create_widgets(
|
||||
"Stroke:",
|
||||
|checkbox: &CheckboxInput| {
|
||||
SplineToolMessage::UpdateOptions {
|
||||
options: SplineOptionsUpdate::StrokeEnabled(checkbox.checked),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
|color: &ColorInput| {
|
||||
SplineToolMessage::UpdateOptions {
|
||||
options: SplineOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
|
||||
options: SplineOptionsUpdate::StrokeColor(color.value.as_solid()),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
));
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
widgets.push(create_weight_widget(self.options.line_weight));
|
||||
widgets.push(Separator::new(SeparatorStyle::Related).widget_instance());
|
||||
let weight_disabled = self.options.drawing.stroke.enabled == Some(false);
|
||||
widgets.push(create_weight_widget(self.options.drawing.line_weight, weight_disabled));
|
||||
|
||||
Layout(vec![LayoutGroup::row(widgets)])
|
||||
}
|
||||
|
|
@ -169,12 +173,18 @@ impl LayoutHolder for SplineTool {
|
|||
#[message_handler_data]
|
||||
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for SplineTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
|
||||
// On tool deactivation (Abort fires from the dispatcher's tool transition), reset the displayed fill/stroke colors so
|
||||
// the next activation starts fresh from the current working colors. The global swap state persists across tool switches.
|
||||
// Guarded on `Ready` so Esc-mid-drawing (which also fires Abort) doesn't wipe the user's customized fill/stroke options.
|
||||
if matches!(&message, ToolMessage::Spline(SplineToolMessage::Abort)) && self.fsm_state == SplineToolFsmState::Ready {
|
||||
reset_colors_on_deactivation(&mut self.options.drawing, context.global_tool_data);
|
||||
}
|
||||
|
||||
if matches!(&message, ToolMessage::Spline(SplineToolMessage::SelectionChanged)) {
|
||||
if self.fsm_state == SplineToolFsmState::Ready
|
||||
&& let Some(weight) = graph_modification_utils::first_selected_stroke_weight(context.document)
|
||||
&& self.options.line_weight != weight
|
||||
{
|
||||
self.options.line_weight = weight;
|
||||
if self.fsm_state != SplineToolFsmState::Ready {
|
||||
return;
|
||||
}
|
||||
if sync_drawing_state(&mut self.options.drawing, false, true, context.global_tool_data, context.document) {
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
}
|
||||
return;
|
||||
|
|
@ -186,24 +196,25 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Spli
|
|||
};
|
||||
match options {
|
||||
SplineOptionsUpdate::LineWeight(line_weight) => {
|
||||
self.options.line_weight = line_weight;
|
||||
graph_modification_utils::set_stroke_weight_for_selected_layers(line_weight, context.document, responses);
|
||||
apply_line_weight(&mut self.options.drawing, line_weight, context.document, responses);
|
||||
}
|
||||
SplineOptionsUpdate::FillColor(color) => {
|
||||
self.options.fill.custom_color = color;
|
||||
self.options.fill.color_type = ToolColorType::Custom;
|
||||
SplineOptionsUpdate::FillColor(fill_choice) => {
|
||||
apply_fill_color_pick(&mut self.options.drawing, fill_choice, context.document, responses);
|
||||
}
|
||||
SplineOptionsUpdate::FillEnabled(enabled) => {
|
||||
apply_fill_enabled(&mut self.options.drawing, enabled, context.global_tool_data, context.document, responses);
|
||||
}
|
||||
SplineOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type,
|
||||
SplineOptionsUpdate::StrokeColor(color) => {
|
||||
self.options.stroke.custom_color = color;
|
||||
self.options.stroke.color_type = ToolColorType::Custom;
|
||||
apply_stroke_color_pick(&mut self.options.drawing, color, context.document, responses);
|
||||
}
|
||||
SplineOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type,
|
||||
SplineOptionsUpdate::WorkingColors(primary, secondary) => {
|
||||
self.options.stroke.primary_working_color = primary;
|
||||
self.options.stroke.secondary_working_color = secondary;
|
||||
self.options.fill.primary_working_color = primary;
|
||||
self.options.fill.secondary_working_color = secondary;
|
||||
SplineOptionsUpdate::StrokeEnabled(enabled) => {
|
||||
apply_stroke_enabled(&mut self.options.drawing, enabled, context.global_tool_data, context.document, responses);
|
||||
}
|
||||
SplineOptionsUpdate::SwapFillAndStroke => {
|
||||
swap_fill_and_stroke(&mut self.options.drawing, context.document, responses);
|
||||
}
|
||||
SplineOptionsUpdate::WorkingColorsChanged => {
|
||||
apply_working_colors(&mut self.options.drawing, context.global_tool_data, context.document);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -311,7 +322,6 @@ impl Fsm for SplineToolFsmState {
|
|||
) -> Self {
|
||||
let ToolActionMessageContext {
|
||||
document,
|
||||
global_tool_data,
|
||||
input,
|
||||
shape_editor,
|
||||
viewport,
|
||||
|
|
@ -359,7 +369,7 @@ impl Fsm for SplineToolFsmState {
|
|||
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
tool_data.cleanup();
|
||||
tool_data.weight = tool_options.line_weight;
|
||||
tool_data.weight = tool_options.drawing.effective_line_weight();
|
||||
|
||||
let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position));
|
||||
let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input, viewport), &point, SnapTypeConfiguration::default());
|
||||
|
|
@ -415,8 +425,8 @@ impl Fsm for SplineToolFsmState {
|
|||
let nodes = vec![(NodeId(1), path_node), (NodeId(0), spline_node)];
|
||||
|
||||
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
|
||||
tool_options.stroke.apply_stroke(tool_data.weight, layer, responses);
|
||||
tool_options.fill.apply_fill(layer, responses);
|
||||
tool_options.drawing.stroke.apply_stroke(tool_data.weight, layer, responses);
|
||||
tool_options.drawing.fill.apply_fill(layer, responses);
|
||||
tool_data.current_layer = Some(layer);
|
||||
tool_data.new_layer_viewport_start = Some(viewport_vec);
|
||||
|
||||
|
|
@ -545,7 +555,7 @@ impl Fsm for SplineToolFsmState {
|
|||
}
|
||||
(_, SplineToolMessage::WorkingColorChanged) => {
|
||||
responses.add(SplineToolMessage::UpdateOptions {
|
||||
options: SplineOptionsUpdate::WorkingColors(Some(global_tool_data.primary_color), Some(global_tool_data.secondary_color)),
|
||||
options: SplineOptionsUpdate::WorkingColorsChanged,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
|
|||
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
|
||||
use crate::messages::portfolio::utility_types::{CachedData, FontCatalog, FontCatalogStyle};
|
||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
|
||||
use crate::messages::tool::common_functionality::color_selector::{
|
||||
ToolColorOptions, apply_fill_only_color_pick, apply_fill_only_enabled, refresh_slot_working_color, selection_changed_since_last_sync, solid_gamma, sync_fill_only,
|
||||
};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::common_functionality::resize::Resize;
|
||||
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData};
|
||||
|
|
@ -20,7 +22,7 @@ use graph_craft::document::{NodeId, NodeInput};
|
|||
use graphene_std::choice_type::ChoiceTypeStatic;
|
||||
use graphene_std::renderer::Quad;
|
||||
use graphene_std::text::{Font, FontCache, TextAlign, TypesettingConfig, lines_clipping};
|
||||
use graphene_std::vector::style::Fill;
|
||||
use graphene_std::vector::style::{Fill, FillChoice};
|
||||
use graphene_std::{Color, NodeInputDecleration};
|
||||
|
||||
#[derive(Default, ExtractField)]
|
||||
|
|
@ -37,6 +39,8 @@ pub struct TextOptions {
|
|||
fill: ToolColorOptions,
|
||||
tilt: f64,
|
||||
align: TextAlign,
|
||||
/// Set of layers we last synced from, used to detect real selection changes vs. internal node toggles.
|
||||
last_synced_selection: Vec<LayerNodeIdentifier>,
|
||||
}
|
||||
|
||||
impl Default for TextOptions {
|
||||
|
|
@ -45,9 +49,10 @@ impl Default for TextOptions {
|
|||
font_size: 24.,
|
||||
character_spacing: 0.,
|
||||
font: Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.into(), graphene_std::consts::DEFAULT_FONT_STYLE.into()),
|
||||
fill: ToolColorOptions::new_primary(),
|
||||
fill: ToolColorOptions::new_enabled(),
|
||||
tilt: 0.,
|
||||
align: TextAlign::default(),
|
||||
last_synced_selection: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -78,12 +83,12 @@ pub enum TextToolMessage {
|
|||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum TextOptionsUpdate {
|
||||
FillColor(Option<Color>),
|
||||
FillColorType(ToolColorType),
|
||||
FillColor(FillChoice),
|
||||
FillEnabled(bool),
|
||||
Font { font: Font },
|
||||
FontSize(f64),
|
||||
Align(TextAlign),
|
||||
WorkingColors(Option<Color>, Option<Color>),
|
||||
WorkingColorsChanged,
|
||||
}
|
||||
|
||||
impl ToolMetadata for TextTool {
|
||||
|
|
@ -263,34 +268,21 @@ impl TextTool {
|
|||
}
|
||||
|
||||
fn layout(&self, font_catalog: &FontCatalog, document: &DocumentMessageHandler) -> Layout {
|
||||
let mut widgets = create_text_widgets(self, font_catalog, document);
|
||||
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
|
||||
widgets.append(&mut self.options.fill.create_widgets(
|
||||
"Fill",
|
||||
true,
|
||||
|_| {
|
||||
TextToolMessage::UpdateOptions {
|
||||
options: TextOptionsUpdate::FillColor(None),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
|color_type: ToolColorType| {
|
||||
WidgetCallback::new(move |_| {
|
||||
let mut widgets = vec![
|
||||
ColorInput::new(self.options.fill.fill_choice.clone().unwrap_or(graphene_std::vector::style::FillChoice::None))
|
||||
.mixed(self.options.fill.fill_choice.is_none())
|
||||
.narrow(true)
|
||||
.on_update(|color: &ColorInput| {
|
||||
TextToolMessage::UpdateOptions {
|
||||
options: TextOptionsUpdate::FillColorType(color_type.clone()),
|
||||
options: TextOptionsUpdate::FillColor(color.value.clone()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
},
|
||||
|color: &ColorInput| {
|
||||
TextToolMessage::UpdateOptions {
|
||||
options: TextOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
));
|
||||
.widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
];
|
||||
|
||||
widgets.extend(create_text_widgets(self, font_catalog, document));
|
||||
|
||||
Layout(vec![LayoutGroup::row(widgets)])
|
||||
}
|
||||
|
|
@ -299,6 +291,13 @@ impl TextTool {
|
|||
#[message_handler_data]
|
||||
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for TextTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
|
||||
// On tool deactivation (Abort fires from the dispatcher's tool transition),
|
||||
// reset the displayed fill color so the next activation starts fresh from the current working color.
|
||||
// Guarded on `Ready` so Esc-mid-editing (which also fires Abort) doesn't wipe the user's customized fill option.
|
||||
if matches!(&message, ToolMessage::Text(TextToolMessage::Abort)) && self.fsm_state == TextToolFsmState::Ready {
|
||||
self.options.fill.fill_choice = Some(solid_gamma(context.global_tool_data.primary_color));
|
||||
}
|
||||
|
||||
let options = match message {
|
||||
ToolMessage::Text(TextToolMessage::UpdateOptions { options }) => options,
|
||||
ToolMessage::Text(TextToolMessage::SelectionChanged) => {
|
||||
|
|
@ -314,6 +313,18 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
|
|||
editing_text.font = font.clone();
|
||||
}
|
||||
}
|
||||
|
||||
// Only sync from a text selection; reading a non-text layer's fill would pollute the swatch
|
||||
let selection_changed = selection_changed_since_last_sync(&mut self.options.last_synced_selection, context.document);
|
||||
if can_edit_selected(context.document).is_some() {
|
||||
sync_fill_only(&mut self.options.fill, true, context.global_tool_data.primary_color, context.document, selection_changed);
|
||||
} else if selection_changed {
|
||||
self.options.fill.fill_choice = Some(solid_gamma(context.global_tool_data.primary_color));
|
||||
self.options.fill.tracks_working_color = true;
|
||||
}
|
||||
// Text tool has no fill checkbox; keep enabled so new text never starts with `None`
|
||||
self.options.fill.enabled = Some(true);
|
||||
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions, &context.cached_data.font_catalog, context.document);
|
||||
return;
|
||||
}
|
||||
|
|
@ -361,14 +372,15 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
|
|||
});
|
||||
}
|
||||
}
|
||||
TextOptionsUpdate::FillColor(color) => {
|
||||
self.options.fill.custom_color = color;
|
||||
self.options.fill.color_type = ToolColorType::Custom;
|
||||
TextOptionsUpdate::FillColor(fill_choice) => {
|
||||
// Text fill is bound to the primary working color (no swap concept).
|
||||
apply_fill_only_color_pick(&mut self.options.fill, fill_choice, true, context.document, responses);
|
||||
}
|
||||
TextOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type,
|
||||
TextOptionsUpdate::WorkingColors(primary, secondary) => {
|
||||
self.options.fill.primary_working_color = primary;
|
||||
self.options.fill.secondary_working_color = secondary;
|
||||
TextOptionsUpdate::FillEnabled(enabled) => {
|
||||
apply_fill_only_enabled(&mut self.options.fill, enabled, context.global_tool_data.primary_color, context.document, responses);
|
||||
}
|
||||
TextOptionsUpdate::WorkingColorsChanged => {
|
||||
refresh_slot_working_color(&mut self.options.fill, context.global_tool_data.primary_color, context.document);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -618,9 +630,10 @@ fn can_edit_selected(document: &DocumentMessageHandler) -> Option<LayerNodeIdent
|
|||
return None;
|
||||
}
|
||||
|
||||
if !document.metadata().is_text_layer(layer) {
|
||||
return None;
|
||||
}
|
||||
// Detect text layers by the presence of a Text proto node in the chain, not via `metadata().is_text_layer()` which is
|
||||
// populated lazily by the renderer after `RunDocumentGraph`. A freshly created text layer's `text_frames` entry isn't
|
||||
// available yet when SelectionChanged fires, so the metadata check would incorrectly classify it as non-text.
|
||||
graph_modification_utils::get_text_id(layer, &document.network_interface)?;
|
||||
|
||||
Some(layer)
|
||||
}
|
||||
|
|
@ -639,7 +652,6 @@ impl Fsm for TextToolFsmState {
|
|||
) -> Self {
|
||||
let ToolActionMessageContext {
|
||||
document,
|
||||
global_tool_data,
|
||||
input,
|
||||
cached_data,
|
||||
viewport,
|
||||
|
|
@ -1036,7 +1048,7 @@ impl Fsm for TextToolFsmState {
|
|||
}
|
||||
(_, TextToolMessage::WorkingColorChanged) => {
|
||||
responses.add(TextToolMessage::UpdateOptions {
|
||||
options: TextOptionsUpdate::WorkingColors(Some(global_tool_data.primary_color), Some(global_tool_data.secondary_color)),
|
||||
options: TextOptionsUpdate::WorkingColorsChanged,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
|
|
|||
|
|
@ -328,7 +328,7 @@
|
|||
.icon-button,
|
||||
.text-button,
|
||||
.popover-button,
|
||||
.color-button > button,
|
||||
.color-input > button,
|
||||
.color-picker .preset-color,
|
||||
.working-colors-input .swatch > button,
|
||||
.radio-input button,
|
||||
|
|
|
|||
|
|
@ -215,6 +215,7 @@
|
|||
$$events: {
|
||||
value: (e: CustomEvent) => widgetValueUpdate(index, e.detail, true),
|
||||
startHistoryTransaction: () => widgetValueCommit(index, props.value),
|
||||
commitHistoryTransaction: () => editor.endTransaction(),
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
export let icon: IconName | undefined = undefined;
|
||||
export let forLabel: bigint | undefined = undefined;
|
||||
export let disabled = false;
|
||||
export let mixed = false;
|
||||
// Tooltips
|
||||
export let tooltipLabel: string | undefined = undefined;
|
||||
export let tooltipDescription: string | undefined = undefined;
|
||||
|
|
@ -22,6 +23,7 @@
|
|||
|
||||
$: id = forLabel !== undefined ? String(forLabel) : backupId;
|
||||
$: displayIcon = !checked && (!icon || icon === "Checkmark") ? "Empty12px" : icon || "Checkmark";
|
||||
$: if (inputElement) inputElement.indeterminate = mixed;
|
||||
|
||||
export function isChecked() {
|
||||
return checked;
|
||||
|
|
@ -43,7 +45,14 @@
|
|||
type="checkbox"
|
||||
id={`checkbox-input-${id}`}
|
||||
bind:checked
|
||||
on:change={(_) => dispatch("checked", inputElement?.checked || false)}
|
||||
on:change={(_) => {
|
||||
// Clicking a mixed-state checkbox always transitions to ticked rather than following HTML's default toggle from the previous `checked` value
|
||||
if (mixed && inputElement && !inputElement.checked) {
|
||||
inputElement.checked = true;
|
||||
checked = true;
|
||||
}
|
||||
dispatch("checked", inputElement?.checked || false);
|
||||
}}
|
||||
{disabled}
|
||||
tabindex={disabled ? -1 : 0}
|
||||
bind:this={inputElement}
|
||||
|
|
@ -51,6 +60,7 @@
|
|||
<label
|
||||
class:disabled
|
||||
class:checked
|
||||
class:mixed
|
||||
for={`checkbox-input-${id}`}
|
||||
on:keydown={(e) => e.key === "Enter" && toggleCheckboxFromLabel(e)}
|
||||
data-tooltip-label={tooltipLabel}
|
||||
|
|
@ -130,6 +140,29 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Mixed (takes priority over both checked and unchecked appearances)
|
||||
label.mixed .checkbox-box,
|
||||
input:checked + label.mixed .checkbox-box {
|
||||
position: relative;
|
||||
background: var(--color-5-dullgray);
|
||||
|
||||
.icon-label {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 8px;
|
||||
height: 2px;
|
||||
transform: translate(-50%, -50%);
|
||||
background: var(--color-8-uppergray);
|
||||
border-radius: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
+ .text-label.text-label {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,12 @@
|
|||
// export let allowTransparency = false; // TODO: Implement
|
||||
export let menuDirection: MenuDirection = "Bottom";
|
||||
export let disabled = false;
|
||||
export let mixed = false;
|
||||
// Styling
|
||||
export let narrow = false;
|
||||
// Sizing
|
||||
export let minWidth = 0;
|
||||
export let maxWidth = 0;
|
||||
// Tooltips
|
||||
export let tooltipLabel: string | undefined = undefined;
|
||||
export let tooltipDescription: string | undefined = undefined;
|
||||
|
|
@ -23,7 +27,7 @@
|
|||
|
||||
let open = false;
|
||||
|
||||
$: outlineFactor = contrastingOutlineFactor(value, ["--color-1-nearblack", "--color-3-darkgray"], 0.01);
|
||||
$: outlineFactor = contrastingOutlineFactor(value, "--color-3-darkgray", 0.01);
|
||||
$: outlined = outlineFactor > 0.0001;
|
||||
$: gradientStops = fillChoiceGradientStops(value);
|
||||
$: solidColor = fillChoiceColor(value);
|
||||
|
|
@ -31,7 +35,17 @@
|
|||
$: transparency = gradientStops ? gradientStops.color.some((color) => color.alpha < 1) : solidColor ? solidColor.alpha < 1 : false;
|
||||
</script>
|
||||
|
||||
<LayoutCol class="color-button" classes={{ open, disabled, narrow, none, transparency, outlined, "direction-top": menuDirection === "Top" }} {tooltipLabel} {tooltipDescription} {tooltipShortcut}>
|
||||
<LayoutCol
|
||||
class="color-input"
|
||||
classes={{ open, disabled, narrow, none, transparency, outlined, mixed, "direction-top": menuDirection === "Top" }}
|
||||
styles={{
|
||||
...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}),
|
||||
...(maxWidth > 0 ? { "max-width": `${maxWidth}px` } : {}),
|
||||
}}
|
||||
{tooltipLabel}
|
||||
{tooltipDescription}
|
||||
{tooltipShortcut}
|
||||
>
|
||||
<button style:--chosen-gradient={chosenGradient} style:--outline-amount={outlineFactor} on:click={() => (open = true)} tabindex="0" data-floating-menu-spawner></button>
|
||||
<ColorPicker
|
||||
{open}
|
||||
|
|
@ -53,7 +67,7 @@
|
|||
</LayoutCol>
|
||||
|
||||
<style lang="scss">
|
||||
.color-button {
|
||||
.color-input {
|
||||
position: relative;
|
||||
min-width: 80px;
|
||||
|
||||
|
|
@ -131,6 +145,28 @@
|
|||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.mixed > button {
|
||||
position: relative;
|
||||
background: var(--color-e-nearwhite);
|
||||
background-image: none;
|
||||
|
||||
&::before {
|
||||
background: var(--color-e-nearwhite);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 8px;
|
||||
height: 2px;
|
||||
border-radius: 1px;
|
||||
transform: translate(-50%, -50%);
|
||||
background: var(--color-8-uppergray);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.disabled):hover > button .text-label,
|
||||
&:not(.disabled).open > button .text-label {
|
||||
background: var(--color-6-lowergray);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
const BUTTON_LEFT = 0;
|
||||
const BUTTON_RIGHT = 2;
|
||||
|
||||
const dispatch = createEventDispatcher<{ value: number | undefined; startHistoryTransaction: undefined }>();
|
||||
const dispatch = createEventDispatcher<{ value: number | undefined; startHistoryTransaction: undefined; commitHistoryTransaction: undefined }>();
|
||||
|
||||
const editor = getContext<EditorWrapper>("editor");
|
||||
|
||||
|
|
@ -84,6 +84,9 @@
|
|||
let shiftKeyDown = false;
|
||||
// Track whether the Ctrl key is currently held down.
|
||||
let ctrlKeyDown = false;
|
||||
// True between dispatching `startHistoryTransaction` and the matching `commitHistoryTransaction`, so we only commit
|
||||
// when this widget actually opened a transaction (skipping clicks-without-drag and aborts-before-drag-started).
|
||||
let transactionInProgress = false;
|
||||
// Cleanup function for active drag interactions, called on destroy to prevent leaked listeners
|
||||
let activeDragCleanup: (() => void) | undefined;
|
||||
// Track the slider abort state for cleanup on destroy
|
||||
|
|
@ -135,8 +138,17 @@
|
|||
removeEventListener("keydown", sliderAbortFromDragging);
|
||||
removeEventListener("keydown", incrementPressAbort);
|
||||
if (sliderResetAbortHandler) removeEventListener("pointerup", sliderResetAbortHandler);
|
||||
|
||||
commitTransactionIfInProgress();
|
||||
});
|
||||
|
||||
function commitTransactionIfInProgress() {
|
||||
if (transactionInProgress) {
|
||||
dispatch("commitHistoryTransaction");
|
||||
transactionInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// TRACKING AND UPDATING THE VALUE
|
||||
// ===============================
|
||||
|
|
@ -243,9 +255,13 @@
|
|||
|
||||
if (newValue !== undefined) {
|
||||
const oldValue = value !== undefined && isInteger ? Math.round(value) : value;
|
||||
if (newValue !== oldValue) dispatch("startHistoryTransaction");
|
||||
if (newValue !== oldValue) {
|
||||
dispatch("startHistoryTransaction");
|
||||
transactionInProgress = true;
|
||||
}
|
||||
}
|
||||
updateValue(newValue);
|
||||
commitTransactionIfInProgress();
|
||||
|
||||
editing = false;
|
||||
self?.unFocus();
|
||||
|
|
@ -477,6 +493,9 @@
|
|||
|
||||
// Clean up the event listeners.
|
||||
activeDragCleanup?.();
|
||||
|
||||
// Close out the transaction `startDragging` opened so the many emits collapse into one history step (covers both confirmed and aborted drags).
|
||||
commitTransactionIfInProgress();
|
||||
};
|
||||
|
||||
addEventListener("pointerup", pointerUp);
|
||||
|
|
@ -626,12 +645,17 @@
|
|||
removeEventListener("keydown", sliderAbortFromMousedown);
|
||||
removeEventListener("pointermove", sliderAbortFromDragging);
|
||||
removeEventListener("keydown", sliderAbortFromDragging);
|
||||
|
||||
// Close out the transaction `startDragging` opened, so the drag's many emits collapse into one history step.
|
||||
// Covers the abort path too (sliderAbort already restored the original value, so the committed step is a no-op).
|
||||
commitTransactionIfInProgress();
|
||||
}
|
||||
|
||||
function startDragging() {
|
||||
// This event is sent to the backend so it knows to start a transaction for the history system. See discussion for some explanation:
|
||||
// <https://github.com/GraphiteEditor/Graphite/pull/1584#discussion_r1477592483>
|
||||
dispatch("startHistoryTransaction");
|
||||
transactionInProgress = true;
|
||||
}
|
||||
|
||||
// We want to let the user abort while dragging the slider by right clicking or pressing Escape.
|
||||
|
|
@ -663,6 +687,8 @@
|
|||
// dragging the slider, now that we're no longer dragging it due to the loss of window focus.
|
||||
removeEventListener("pointermove", sliderAbortFromDragging);
|
||||
removeEventListener("keydown", sliderAbortFromDragging);
|
||||
|
||||
commitTransactionIfInProgress();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -343,7 +343,12 @@ function detectShake(e: PointerEvent | MouseEvent): boolean {
|
|||
}
|
||||
|
||||
function targetIsTextField(target: EventTarget | HTMLElement | undefined): boolean {
|
||||
return target instanceof HTMLElement && (target.nodeName === "INPUT" || target.nodeName === "TEXTAREA" || target.isContentEditable);
|
||||
if (!(target instanceof HTMLElement)) return false;
|
||||
return (
|
||||
target.isContentEditable ||
|
||||
target instanceof HTMLTextAreaElement ||
|
||||
(target instanceof HTMLInputElement && ["text", "password", "email", "url", "tel", "search", "number", "date", "datetime-local", "month", "time", "week"].includes(target.type))
|
||||
);
|
||||
}
|
||||
|
||||
function potentiallyRestoreCanvasFocus(e: Event) {
|
||||
|
|
|
|||
|
|
@ -334,9 +334,18 @@ impl EditorWrapper {
|
|||
pub fn widget_value_commit_and_update(&self, layout_target: JsValue, widget_id: u64, value: JsValue, resend_widget: bool) -> Result<(), JsValue> {
|
||||
self.widget_value_commit_helper(layout_target.clone(), widget_id, value.clone())?;
|
||||
self.widget_value_update_helper(layout_target, widget_id, value, resend_widget)?;
|
||||
// Close out a transaction that the widget's `on_commit` opened (if any), so a single click on widgets like the
|
||||
// NumberInput's increment buttons collapses into one history step instead of leaving the transaction in `Modified`
|
||||
self.dispatch(DocumentMessage::EndTransaction);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Closes out the current transaction (drag-end / text-commit end), so emits during a slider drag collapse into one history step instead of N
|
||||
#[wasm_bindgen(js_name = endTransaction)]
|
||||
pub fn end_transaction(&self) {
|
||||
self.dispatch(DocumentMessage::EndTransaction);
|
||||
}
|
||||
|
||||
pub fn widget_value_update_helper(&self, layout_target: JsValue, widget_id: u64, value: JsValue, resend_widget: bool) -> Result<(), JsValue> {
|
||||
let widget_id = WidgetId(widget_id);
|
||||
match (from_value(layout_target), from_value(value)) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue